TypeScriptのasync/awaitとPromise

非同期処理を同期的に書く際に便利な、async/await 構文について紹介します。


async/await 構文の使い方

非同期処理が終わるまで待ってくれる構文です。例えば、XMLHttpRequest を使って JSON ファイルを読み込むような処理を、以下のように書くことができます。

start 関数は、data.json という JSON ファイルを読み込み、結果をコンソールに出力する関数です。loadData 関数は、url を受け取り、データを読み込んで、JSON をパースして返してくれる関数です。説明の都合上、start 関数を非同期処理呼び出し関数、loadData 関数を非同期関数と呼びます。さて、使い方のポイントは、以下の 5 つです。

  1. 非同期処理呼び出し関数には、async をつける
  2. 非同期関数は、Promise オブジェクトを返す
  3. 非同期処理が終わったら、resolve を呼ぶ
  4. 非同期関数の戻り値は、resolve に渡す
  5. 非同期関数を呼び出す際には、await をつける

以上のように書くと、start 関数内では、loadData 関数の処理が終わる(正確には、resolve が呼び出される)まで待ってから、返り値を受け取り、次の処理に進むようになります。それなりに書くことが多いのですが、それでも、async/await を使わないよりは、随分と楽になります。

非同期関数のテンプレート

先に書いた通り、await で待つ関数は、必ず Promise オブジェクトを生成して返すようにします。Promise を生成する際には、最初に処理したい関数を渡します。最初に処理したい関数の第一引数には処理が成功した際に呼び出す関数が、第二引数には失敗した際に呼び出す関数が渡されます(第二引数は省略可能です)。さて、それを踏まえると、非同期関数以下のようになります。

TypeScript のアロー構文を使うならこんな感じです。

あとは、完了後に第一引数として渡される resolve 関数を呼び出せば OK です。await で呼び出された際に返す返り値は、resolve の引数に渡します。

失敗したときの処理

もし、処理に失敗した場合は、reject を呼び出します。すると、呼び出し側の例外に引っかかるようになります。たとえば、以下のような感じです。

以上のように書くことで、成功時とエラー時の分岐も記述することができます。

async/await の利用例

基本的な使い方を説明したところで、いくつかの利用例を紹介していきます。

async/await を使ってファイルを順番に読む

async/await を使うと、ループ構文を使って順番にファイルを読み込んでいくような処理もシンプルに記述できます。たとえば、以下のとおりです。

このようにすることで、data1 ~ 5.json というファイルを順番に読んでいくことができます。読み込み順を制限したい場合は便利でしょう。

FileAPI を使って、ディレクトリ下を列挙

FileAPI を使うことで、ディレクトリ下のファイルを列挙することができますが、列挙関数は非同期で動作します。async/await 構文を使うことで、逐次処理的に動作させることも可能になります。

EnumFileEntry 関数を呼び出すことで、FileEntry 下のファイル及びディレクトリを深さ優先で探索していくことができます。EnumFileEntry 関数は Promise を返すため、それ自体を await で呼び出し可能です。

EnumFileEntry 関数の返す Promise に渡す関数自体も、async 関数になっており、その中では更に別の Promise オブジェクトを待ちます。さらにさらに、ディレクトリ下を列挙する readEntries 関数に渡す関数も async 関数で、その中では EnumFileEntry 関数を再帰的に呼び出しています。(async キーワードは、=> 演算子にも使うことができます。また、ここでは少々横着をして、生成した Promise オブジェクトを直接 await で待っています。可読性的にどうかはわかりませんが、こういう使い方も可能です。)

ここまで来ると、分かりやすいか?という気もしますが、async/await 無しで書こうと思うとちょっと頭が追いつきません…

注意

await は、関数の処理をブロックしているようにも見えますが、実際には await を呼んだ時点でその関数から抜けていると考える必要があります。await で待っている Promise が解決した後に、そこに戻ってくるだけです。そのため、たとえば、mouse down で await して待っても、解決前に mouse move や mouse up が呼ばれることがあります。同期的に書けるようにはなりますが、Thread の join のようにブロックしているわけではないため、そのことを忘れと思わぬバグに陥ることがあります。

まとめ

TypeScript の便利な構文、async/await について紹介しました。ファイル操作などを非同期で取り扱ってきた人(筆者)からすると、これなしではというくらい便利な構文ではないかと思います。最後に、筆者の async/await の理解をまとめておきます。

  • await は、async 関数内でしか使えない
  • await は、Promise を待つもの