ハイブリッドアプリを作っていて一番悩みやすいポイントは、非同期処理にあるのではないでしょうか。Web APIを使って外部からデータを取得したり、ファイルを書き込んだりする際には、この非同期処理が使われます。

async/awaitの登場によって大幅に書きやすくなっていますが、さらに改善は進んでいます。今回はfor await ofの書き方について解説します。

for await ofとは

forは皆さん使ったことがあるかと思います。whileとともに、繰り返し処理でよく使われる構文です。以下のような書き方が一般的でしょう。

for (let i = 0; i < 100; i++) {

}

この繰り返し処理の部分において非同期処理を有効にするのが for await of 構文になります。書き方としては次のようになります。

for await (obj of iteration) {

}

この for await of の書き方をもう少し詳しく見ていきましょう。

イテレーションとは何か

上記コードで使われているiterationは、イテレーションと呼びます。配列のような形でオブジェクトを複数保持していて、呼ばれる度に次のオブジェクトを返却します。オブジェクトがなくなったら終了という仕組みです。

for await of においても、forが自動的に次のデータ、次のデータと呼び出してくれることで、最終的にデータがなくなった段階で終了になります。

このイテレーションは、Generatorというオブジェクトが実体になります。Generatorオブジェクトは function* という形で定義します。見た目は関数に似ています。

function* gen() {

}

そして、値を返す時には yield という構文を使います。例えばこんなジェネレーターを定義します。

function* gen() {
  yield 'Hello'
  yield 'World'
  yield 'From'
  yield 'Monaca'
}

これを使う際には、まず関数を実行します。

const g = gen()

後は g.next() で結果が返ってきます。

g.next()
// {value: "Hello", done: false}

さらにもう一度実行すると、結果が次のyieldに進みます。

g.next()
// {value: "World", done: false}

done: false と書かれていますが、最後までいくと done: true になります。これで繰り返し処理が終了したというのが分かります。ではこのジェネレーターfor 文に使ってみます。

for (let v of gen()) {
  console.log(v)
}

そうすると結果は次のようになります。関数をfor文の中で使い、まるで配列のように返せるのが分かるかと思います。

Hello
World
From
Monaca

非同期処理でどう使うのか

このジェネレータを使ったイテレーション処理において非同期処理をサポートしたのが for await of 構文になります。

今回は処理を非同期にするために、数秒間処理待ちする関数を作ります。通常、ここはファイル書き込みであったり、fetch関数でのネットワーク処理になります。

const sleep = (s) => new Promise(f => setTimeout(f, s * 1000));

そしてジェネレーターを定義するのですが、asyncを使うのがポイントです。そうすればジェネレーターの中でもawaitを使えます。

async function* gen_async() {
  await sleep(1)
  yield 'Hello'
  await sleep(5)
  yield 'World'
  await sleep(2)
  yield 'From'
  await sleep(1)
  yield 'Monaca'
}

そして呼び出し側で使うのが for await of 構文になります。

(async () => {
  for await (const v of gen_async()) {
    console.log(v);
  }
})();

この処理は順番通り、数秒間処理待ちしながら実行されていきます。

Hello
World
From
Monaca

使い分け

非同期処理の実装としては、下記のような方法があるかと思います。

  • コールバック
  • Promise
  • async/await

コールバックはCordovaプラグインでよく使われる記述方法ですが、現状では主流ではありません。コールバック方式をPromiseに置き換える方法もよく紹介されていますので、コールバック方式のまま利用することは多くないでしょう。

Promiseはthen/catchを使った方式になります。この方法を使った場合、コードのネストが深くなってしまうという難点があります。古いWebブラウザでも使えるので安心できる反面、ハイブリッドアプリにおいてはスマートフォンの最新Webブラウザ(のレンダリングエンジン)が対象になるので、そうした面は気にすることはないでしょう。

Promiseの利点としては、Promise.allを使うことで非同期処理が並列で実行できる点にあります。つまり複数の非同期処理(ネットワーク処理、画像の取得、複数ファイルへの書き込みなど)をまとめて実行できます。これによって処理の高速化が期待できます。

async/awaitを使うのは、コードの見通しがよくなり、非同期処理がまるで通常の同期処理のように書けるのが利点です。

難点としては、関数の呼び出し元で必ずasyncと定義しないといけないこと、さらに処理が直列になってしまうことでしょう。await処理が終わるまで次の処理が行われないので、forを使ったループ処理内でのasync/awaitはお勧めされていません。

まとめ

非同期処理を分かりやすく書くことができれば、アプリのコードがぐっとメンテナンスしやすくなり、保守や開発も容易になります。

ジェネレーターオブジェクトを覚えておくと、forを使った繰り返し処理などが綺麗に書けるようになるでしょう。そして、その中で非同期処理を使う場合には for await of を使って書いてみてください。

for await...of - JavaScript | MDN