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で全て置き換えというわけではなく、状況に応じて使い分けることができると思う。

参考:

developer.mozilla.org