async/awaitをちょっと詳しく解説する
初めてAsync/Awaitを触った時、よくわからないまま使っていたので反省も込めて少し解説。
asyncの定義
async function name([param[, param[, ... param]]]) { statements }
name
:関数名param
:関数に渡す引数名statements
:関数の本体を構成するステートメント
Async/AwaitはNode7.6.0から実装された。 functionの頭にasyncをつけると、その関数を呼び出した時にPromiseを返すようになる。 戻り値がある場合は、Promiseは返された値でresolveする。
ちなみにもう少し厳密にいうと、返り値はAsyncFunction
オブジェクトらしいが、普通に使う分にはあまり気にしなくて良い。
awaitの定義
[rv] = await expression;
expression
:解決を待つ値rv
:解決されたPromiseの値。expressionがPromiseではない場合はその値自体を返す。
awaitはasync functionの内部でのみ利用可能。 await式はasync functionの実行を一時停止し、promiseの解決を待つ。 値がpromiseでない場合は解決されたpromiseに変換する。 promiseがrejectされた場合は理由となった値をスローする。
Promise.thenとasync/awaitは違う使い方?
結論から言うと、違う。 例えばこんなサンプルを考えてみよう。
//asyncsample.js const Promise = require('promise'); function promisesample(){ return new Promise((resolve, reject) => { //1秒後にresolve setTimeout(() => { console.log("resolve!!"); resolve(true); }, 1000); }); } async function execPromise(){ console.log("Before await"); const result = await promisesample(); console.log("After await"); console.log("Result:", result); } function execPromise2(){ console.log("Before Promise"); promisesample().then((result) => { console.log("Result:", result); }); console.log("After Promise"); } let execfunc; if (process.argv[2] == 1) { console.log("Async/Await"); execfunc = execPromise; } else if(process.argv[2] == 2) { console.log("Promise"); execfunc = execPromise2; } execfunc();
コマンドライン引数に1を渡すとAsync/Await、2を渡すとPromiseを実行するコード。
Async/Awaitで実行した場合、
$ node asyncsample.js 1 Async/Await Before await resolve!! After await Result: true
awaitで待っているPromiseがresolveされるまで、次のステートメントには進んでいないことがわかる。
一方、Promise実行では、
$ node asyncsample.js 2 Promise Before Promise After Promise resolve!! Result: true
と、resolveされる前に次のステートメントに進んでしまう。
なんで結果が変わる?
先述の通り、awaitは「Async functionを一旦停止させる」と書いてある。 この説明と実行結果を見れば、
- なぜAwaitはAsync function内でなければ使えないのか
- なぜトップレベルで使えないのか
という理由に納得が行くと思う。
なぜAwaitはAsync function内でなければ使えないのか
Awaitの定義がそうなっているから...という説明はそのまますぎるので少し解説。
Awaitは対象のPromiseがResolveされるまでAsync functionを止める。 概念として少しややこしいが、「ノンブロッキングな状態でブロッキングをする」という説明が正しいだろうか。 狭義のブロッキングとか命名しておこう。(なんかかっこいいので) Async functionが狭義のブロッキングをされてもなんの問題もない。 なぜなら、そのAsync functionの外側ではイベントループが止まらずに回り続けるからだ。
なぜAwaitはトップレベルで使えないのか
上記の通り、AwaitはAsync functionの中で狭義のブロッキングを行う。 仮にもしトップレベルでAwaitできてしまったら、処理全体をブロッキングできてしまうことになり、Nodeのメリットを全て潰すことになる。 トップレベルでは必ずPromiseという形で処理してやる必要がある。 Promiseであればresolveを待たずに次のステートメントに進めるからだ。
なんだか分かりにくいポイント
async/awaitは立場が対等ではないところだと思う。 もう少し具体的に言うと、awaitはasyncがいないと生きることができないが、asyncは必ずしもawaitを必要としない。
async
対象の関数がPromiseを返すようによしなにやってくれる。 この関数を利用する場合はawaitを使ってもいいし、thenチェインを使ってもいい。 asyncは単体で使っても良い。
await
asyncの中で、Promiseな処理のresolve/rejectを(ブロッキングしながら)待つ。
async/awaitとよく書かかれるので密結合しているかと思いきや、 awaitが勝手にasyncに依存しているだけ。
おわりに
上記の内容を理解できれば、PromiseはAsync/Awaitで全て置き換えというわけではなく、状況に応じて使い分けることができると思う。
参考: