65
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

鶏(async)と卵(await)、どちらが先か?

Last updated at Posted at 2021-07-20

[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();
65
47
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?