今までJavaScriptのasync/awaitを、理解が曖昧なまま何となく使っていて、うまく行かない時はPromiseを使ったりしていました。
しかし、最近はasync/awaitが使われているのをよく目にするようになってきたため、もう少しちゃんと理解しないといけないかなぁと思いはじめ、その理解メモです。
async/awaitの基本
まずは基本から。async/awaitを使う目的は、非同期処理を同期処理っぽく、順番に記述できるところでしょうか。
基本的な機能は下記のようなところです。
- Promiseを返す関数をawaitで呼び出すと、次の処理には移らず、Promiseに格納した実値(resolve・rejectで返される値)が確定するまで待っていてくれる。
- awaitは、PromiseからPromiseに格納した実値を取り出して返してくれる。
- Promiseを返す関数がrejectした場合は、awaitは例外を返す。
- awaitの例外はtry/catchのcatchで受ける。
- awaitを使う関数はasync関数でなければいけない。
コードにすると下記のようになります。よく見るパターンです。
// Promiseを返す関数 function test_promise(v){ return new Promise((resolve, reject)=>{ if(v){ resolve("OK"); } else { reject("ERROR"); } }); } (async ()=>{ try{ const ret = await test_promise(true); console.log(ret); // 「OK」と表示される }catch(err){ console.log(err); // 「ERROR」と表示される } })();
async関数とは?
async関数とは要約すると「Promiseを返す関数の別の書き方」ってとこでしょうか。
return ~
が、Promiseでいうところのresolve(~)
に相当する。throw ~
が、Promiseでいうところのreject(~)
に相当する。
例えば先程のPromiseを返す関数test_promise()
をasyncで書き直すと下記のようになります。
async function test_async(v){ if(v){ return "OK"; } else { throw "ERROR"; } }
async関数の注意点
じゃあPromiseを全てasync関数に置き換えらるかというと残念ながら無理です…。
async関数の中で非同期関数を呼び、その中からPromise・throwを返すことができません。
例えば
function test_promise(v){ return new Promise((resolve, reject)=>{ setTimeout(()=>{ if(v){ resolve("OK"); } else { reject("ERROR"); } }, 1000); }); }
のようなPromiseを返す関数を
async function test_async(v){ setTimeout(()=>{ if(v){ return "OK"; // test_async()の戻り値じゃない } else { throw "ERROR"; } }, 1000); }
と書いてもうまくいきません。
無理やりasyncでも成立するようにさせると下記のようになります。
async function test_async(v){ try{ const ret = await new Promise((resolve, reject)=>{ setTimeout(()=>{ if(v){ resolve("OK"); } else { reject("ERROR"); } }, 1000); }); return ret; }catch(err){ throw err; } }
もはや、Promiseでいいじゃんといった感じです。
async関数の中では、awaitを使い逐次処理していき、最後に明示的にreturn/throwで結果を返すのが、一番オーソドックスな使い方のようです。
asyncの中で使う関数全てがPromiseを返してくれるのだったなら、awaitだけでシンプルに書けるので、asyncを使うかPromiseを使うかはケースバイケースかなと思います。
Array.map()の中で非同期処理を行う
配列の各要素を使って非同期処理を行い、全ての処理が終わるまで待ってもらい、処理結果を配列で受け取るやり方です。このパターンはちょいちょい見かけます。
方法
- mapから呼び出す関数をasyncにする。
- async関数では実行結果をreturnで返す(async関数なので、returnで返された値はPromiseとなって返される)。
- mapから生成された配列をPromise.allに格納する。
- Promise.allをawaitで受ける。
コードにすると下記のようになります。
function test_promise(v){ new Promise((resolve, reject)=>{ setTimeout(()=>{ resolve(v*10); }, 1000); }); } (async ()=>{ try{ const array = [1, 2, 3]; const ret = await Promise.all( array.map( async (v)=>{ return test_promise(v); } )); console.log(ret); // [10, 20, 30] と表示される }catch(err){ console.log(err); } })();
async/awaitがどう動作しているか見ていく前に、Promise.all()についておさらいを…
Promise.all()についておさらい
- Promise.all()は、Promiseの配列を引数に取る。
- 全てのPromiseが成功した場合は、then(val)で受けて、引数(val)は結果の配列となる。
- 1つでも失敗した場合は、catch(err)で受けて、引数(err)は最初の失敗のreject(err)の値が入る。
以上を踏まえてコードを振り返ると…
- array.map()は各要素に対して非同期処理を実行し、その実行結果をPromiseとして返すので、array.map()はPromiseの配列となる。
- Promise.all()で上記のPromiseの配列を受ける。
- 全ての処理が成功した場合は、awaitがPromiseの配列を、結果の配列へと展開する。
- 1つでも処理が失敗になった場合は、awaitで例外が投げられて、catchで受ける。
の一連の処理が行われ、結果の配列がawaitから返され、retへと代入されます。
asyncによりPromiseの配列ができるところと、Promiseの配列から次の処理に移らないようawaitで足止めし、かつ、Promiseから値に展開するために、配列をPromise.all()で受けるところがポイントですね。
Array.forEach()の中で非同期処理を行う
Array.map()がうまくいったので、Array.forEach()でもうまくいくかも!?と思いたいところですが、結論から言うとできないです。
例えば、
function test_promise(v){ return new Promise((resolve, reject)=>{ setTimeout(()=>{ console.log(v); resolve(); }, 1000); }); } (async ()=>{ try{ const array = [1, 2, 3]; await array.forEach( async (v)=>{ await test_promise(v); }); console.log("END"); } catch(err){ console.log(err); } }
の結果は
1 2 3 END
とはならず
END 1 2 3
になります。
意図した挙動にするには、Array.forEach()は諦めて、地道にfor/ofを使うしかないです。
(async ()=>{ try{ const array = [1, 2, 3]; for(const v of array){ await test_promise(v); } console.log("END"); } catch(err){ console.log(err); } }
もしくは、前述のArray.map()を、Array.forEach()代わりに使ってもできますね。
(async ()=>{ try{ const array = [1, 2, 3]; await Promise.all( array.map( async (v)=>{ await test_promise(v); return; })); console.log("END"); } catch(err){ console.log(err); } }
「await Promise.all(〜)」を抜けた時点で、「return」が完了、つまり、「async(v)=>{}」内の処理が完了しています。
しかも、非同期で実行されるので、1000msで全てが完了します。一方「for/of」の方は、1つ1つawaitで完了を待って実行するので、全て完了するのに3000msかかります。
ザックリですが、以上である程度async/awaitは使えるようになるのではないでしょうか。
感想
async/awaitを使えばPromiseは必要無くなるかと思ったのですが、そうはいかないようですね。
awaitで全体の見通しが良くなるのですが、並列処理できるところをついついawaitで逐次処理にしてしまって、速度が遅くなることもありますね。