「[JS]なぜawaitはasync関数の中にしか書けないのか」の記事の補足というか、おまけ記事です。
#鶏と卵
多くの人は、「awaitを使いたい」がまず先にあると思います。
const data = await doAjax();
上記の書き方に「これは便利そうだぞ?」とビビッとくるわけです。
しかしその後、次のルールを知って「ん? なんかめんどくさそうだな?」となります。
awaitの先は、Promiseを返す関数か、async関数でなくてはならない
Promiseを返す関数というのはつまりこういうことです。
function sleep(seconds) {
return new Promise(resolve =>
setTimeout(() => resolve(), seconds * 1000)
);
}
このsleep関数をこんな風に使えます。awaitはasync関数の中でしか使えないので、わざわざasyncな無名関数を定義して即時呼び出ししています。
(async () => {
console.log("start");
await sleep(2);
console.log("hello");
})();
そして「なるほど、awaitは、戻ってくるPromiseの解決を待ってくれるわけか」と理解します。
しかし、次に「もうひとつの、"async関数"ってなんだ?」となるはずです。
そしてasync関数について調べると、次のことが分かります。
async関数は、関数の返り値をPromise.resolve()で包みます。
「どういうこと?」という感じです。
例えば、"hello"と返すだけの関数があったとします。
function hello(){
return "hello";
}
const msg = hello();
console.log(msg);
出力
hello
このhello関数の戻り値を、Promise.resolve()で包んでみます。
resolveされたPromiseが返るので、thenで受けなくてはなりません。
function test(){
return Promise.resolve("hello");
}
test().then((msg)=>{
console.log(msg)
});
出力
hello
これを自動的にやってくれるのがasync関数だ、ということです。
async function test(){
return "hello";
}
test().then((msg)=>{
console.log(msg)
});
「なるほど。それで、それは何のためにそうするんだ? awaitからはこの関数しか呼び出せないというのはどういうことだ?」という感じですが、asyncの中でawaitを使うと意味がわかってきます。
async function test(){
const msg = await getMessageAsync();
return msg;
}
test().then((msg)=>{
console.log(msg)
});
getMessageAsync()は、内部でサーバにメッセージを問い合わせる非同期関数です。呼び出すとPromiseを返し、サーバからの問い合わせが返った時にそのPromiseを完了状態にします。
return msg
が実行されるのはawait getMessageAsync()
のPromiseが完了になった後ですから、それまでは、test()が返すPromiseは待機状態のままです。そして、getMessageAsync()が完了になった後、return msg
が、Promiseを完了状態にしてmsgを渡します。
こういった、本来ややこしいPromiseの管理を、asyncとawaitが肩代わりしてくれるわけですが、ここで重要なのは、「awaitがなければasyncにはほぼ意味がない」ということです。
言い換えると、awaitを中で呼び出す為のasync関数であって、async関数を呼び出す為のawaitではないということになるでしょう。
この理解の前に「awaitで呼び出せるのはasync関数」と聞くと、まるで「asyncが先にあって、それを扱う為のawait」のように錯覚してしまいますが、そうではなく、awaitはあくまでもPromiseを同期的な書き方で扱うためのものです。そしてawaitを適切に処理する為に、asyncが存在します。
async関数はPromiseを返すので、結果的にawaitはasync関数も扱える、というだけですね。非常に便利な仕組みだと思います。
ちなみに、何も返さない関数をasyncにしたらどうなるかというと、ちゃんとPromise.resolve()が返ります。
async function test(){
}
test().then((msg)=>{
console.log("done!")
});
なので、次のようにawaitを次々と呼び出すような場合も、単にそのまま処理を終わればOKです。
async function test(){
const data1 = await longProcAsync1();
const data2 = await longProcAsync2(data1);
const data3 = await longProcAsync3(data2);
await lastProcAsync(data3);
}
test().then((msg)=>{
console.log("done!")
});
というわけで、長くなりましたが、結論は以下の通りです。
鶏(async)と卵(await)は、卵(await)が先である。
awaitがやりたいからのasync、ということでした。
#おまけ:asyncとは結局何か
最初にasyncについてこう書きました。
async関数は、関数の返り値をPromise.resolve()で包みます。
しかし、前回の記事を読めばわかるように、実際はそれだけでは全然なくて、
asyncは、それを付けた関数をawaitからawaitまでのブロックで分割したジェネレータに変換し、各awaitが待つPromiseが完了状態になった時に次のブロックへと処理を進め、関数が終了した時に結果をPromise.resolve()で包んで返します。
ということでした。async、すごいやつです。
#おまけ2:なぜawaitを最上位コードに書けないのか?
(追記 2021/07/21)なんと、もうすぐ最上位にawaitを書けるようになるかもしれないそうです(一部ブラウザは実装済)。コメント欄で教えて頂きました。感謝!
ちなみに、そろそろasync/awaitにも慣れてきて、次のように書きたくなってきたかもしれません。
async function test(){
const msg = await getMessageAsync();
return msg;
}
const msg = await test();
console.log("done")
しかしこれはエラーになって実行できません。なぜならjavascriptコンパイラは、async関数に入っていないawait test()
をジェネレータに変換できない為です(最上位のコード全体をまとめて1つのジェネレータに変換すれば可能かもしれませんが、さすがにそれはやりすぎということでしょう)。
これなら大丈夫です。
async function test(){
const msg = await getMessageAsync();
return msg;
}
async function main(){
const msg = await test();
console.log("done")
}
main();