非同期処理を同期的(?)に書く際に便利な、async/await構文について紹介します。
async/await構文の使い方
非同期処理が終わるまで待ってくれる構文です。例えば、XMLHttpRequestを使ってJSONファイルを読み込むような処理を、以下のように書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function loadData(url: string) { return new Promise(resolve => { let xhr = new XMLHttpRequest(); xhr.onload = ()=>{ resolve(JSON.parse(xhr.response)); }; xhr.open("GET", url); xhr.send(null); }); } async function start() { let obj = await loadData("./data.json"); console.log(obj); } start(); |
start関数は、data.jsonというJSONファイルを読み込み、結果をコンソールに出力する関数です。loadData関数は、urlを受け取り、データを読み込んで、JSONをパースして返してくれる関数です。説明の都合上、start関数を非同期処理呼び出し関数、loadData関数を非同期関数と呼びます。さて、使い方のポイントは、以下の5つです。
- 非同期処理呼び出し関数には、asyncをつける
- 非同期関数は、Promiseオブジェクトを返す
- 非同期処理が終わったら、resolveを呼ぶ
- 非同期関数の戻り値は、resolveに渡す
- 非同期関数を呼び出す際には、awaitをつける
以上のように書くと、start関数内では、loadData関数の処理が終わる(正確には、resolveが呼び出される)まで待ってから、返り値を受け取り、次の処理に進むようになります。それなりに書くことが多いのですが、それでも、async/awaitを使わないよりは、随分と楽になります。
[topBannar]
非同期関数のテンプレート
先に書いた通り、awaitで待つ関数は、必ずPromiseオブジェクトを生成して返すようにします。Promiseを生成する際には、最初に処理したい関数を渡します。最初に処理したい関数の第一引数には処理が成功した際に呼び出す関数が、第二引数には失敗した際に呼び出す関数が渡されます(第二引数は省略可能です)。さて、それを踏まえると、非同期関数以下のようになります。
1 2 3 4 5 6 7 8 |
function asyncFunc() { return new Promise( function(resolve, reject){ //実際の処理 }); } |
TypeScriptのアロー構文を使うならこんな感じです。
1 2 3 4 5 6 7 8 |
function asyncFunc() { return new Promise( (resolve, reject) => { //実際の処理 (終わったら、resolveを呼ぶ) }); } |
あとは、完了後に第一引数として渡されるresolve関数を呼び出せばOKです。awaitで呼び出された際に返す返り値は、resolveの引数に渡します。
失敗したときの処理
もし、処理に失敗した場合は、rejectを呼び出します。すると、呼び出し側の例外に引っかかるようになります。たとえば、以下のような感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function loadData(url: string) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.onload = ()=>{ resolve(JSON.parse(xhr.response)); }; //エラー時には rejectを呼びます xhr.onerror = ()=>{ reject(xhr.response); }; xhr.open("GET", url); xhr.send(null); }); } async function start() { try{ let obj = await loadData("./data.json"); console.log(obj); }catch(e) { //eには、xhrが入っているはずです。 console.log(e); } } |
以上のように書くことで、成功時とエラー時の分岐も記述することができます。
async/awaitの利用例
基本的な使い方を説明したところで、いくつかの利用例を紹介していきます。
async/awaitを使ってファイルを順番に読む
async/awaitを使うと、ループ構文を使って順番にファイルを読み込んでいくような処理もシンプルに記述できます。たとえば、以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function loadData(url: string) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.onload = ()=>{ resolve(JSON.parse(xhr.response)); }; //エラー時には rejectを呼びます xhr.onerror = ()=>{ reject(xhr.response); }; xhr.open("GET", url); xhr.send(null); }); } async function start() { var fileList = [ "./data1.json", "./data2.json", "./data3.json", "./data4.json", "./data5.json", ]; for (let file of fileList) { try { let obj = await loadData(file); console.log(obj); } catch (e) { //eには、xhrが入っているはずです。 console.log(e); } } } |
このようにすることで、data1~5.jsonというファイルを順番に読んでいくことができます。読み込み順を制限したい場合は便利でしょう。
FileAPIを使って、ディレクトリ下を列挙
FileAPIを使うことで、ディレクトリ下のファイルを列挙することができますが、列挙関数は非同期で動作します。async/await構文を使うことで、逐次処理的に動作させることも可能になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
async function EnumFileEntry(entry: WebKitFileEntry, outList: WebKitFileEntry[]) { return new Promise( async (resolveAll, rejectAll) => { outList.push(entry); try { if (entry.isDirectory) { await new Promise((resolve, reject) => { var dirReader: WebKitDirectoryReader = (<any>entry).createReader(); dirReader.readEntries( async (entries: any) => { for (let i = 0; i < entries.length; i++) { let c = <WebKitFileEntry>entries[i]; await EnumFileEntry(c, outList); } resolve(); }, () => { reject(); } ); }); } resolveAll(); } catch (e) { rejectAll(); } }); } |
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を待つもの
[bottomBannar]