ハイブリッドアプリを作っていて一番悩みやすいポイントは、非同期処理にあるのではないでしょうか。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
を使って書いてみてください。