はじめに
async/awaitを使っていて制御の順番が思い通りにならなくて困ったことは誰しもあると思います。
そんな方のために、awaitの挙動からなぜ書いたコードが思い通りの順番で実行されないのか少しだけ深掘りしていきます。
今回はsetTimeout()
編です。
目次
- async/awaitとは
- 待ってくれないawait
- awaitはPromiseインスタンスの値を取り出す
- setTimeout()はPromiseじゃない
- awaitにPromise以外を待たせようとしても待ってくれない
- それでも待たせたいなら
async/awaitとは
async/awaitとは、非同期処理を行う際に処理の順番をコントロールするために使う記述です。
例えば以下のようにasync関数内でfetch()
を行う関数を実行すると、コード上の順番と処理の実行順がずれます。
const asyncFunction = async(url) => {
const data = await fetch(url).data;
console.log(data);
}
console.log("start");
asyncFunction(取得したいURL);
console.log("finish");
start
finish
(取得したデータ)
取得したデータのコンソールへの出力が最後になりましたね。
待ってくれないawait
このような例から、await式は「非同期処理の完了を待つ」記述だといわれます。
しかし本質的には、「Promiseインスタンスの値を取り出す」という操作が行われています。
これをきちんと理解しないと、「非同期処理なのにawaitが待ってくれない」という残念な現象が発生します。
const asyncFunction = async() => {
await setTimeout(() => console.log("Please wait"), 1000)
console.log("Cannot wait");
}
console.log("start");
asyncFunction();
console.log("finish");
start
finish
Cannot wait
---
(1秒後)
Please wait
確かに"start"→asyncFunction
→"finish"という順番がstart→finish→asyncFunction
という順番になりましたが、"Cannot wait"が"Please wait"を待たずして出力されているため、肝心のasyncFunction
内部ではawaitが機能していないようです。
なぜなのか順に説明していきます。
awaitはPromiseインスタンスの値を取り出す
awaitの機能をもう少し正確に記述すると、それは 「右にあるPromiseの確定した値を評価する(取り出す)」 という機能になります。
ここで一つ押さえておきたいのがPromiseについてです。
Promiseインスタンスは3つの State(状態) を持ち、それぞれ
- Pending(待機状態)
- Fulfilled(履行状態)
- Rejected(拒否状態)
があります。Pendingはその名の通り、まだ実行されていない状態ですが、FulfilledとRejectedは値が確定し、これ以上変わらなくなります。
例えばそれは、fetch()
で正しくデータを取得できた、あるいはできずに例外が発生した時だとか、Promise.resolve()
やPromise.reject()
されたときなどです。
awaitはPromiseインスタンスがFulfilledやRejectedになって確定した値を評価しよう(取り出そう)と「待つ」わけです。
setTimeout()はPromiseじゃない
実は、setTimeout()
は確かに非同期処理を行うことができますが、Promiseではないのです。
console.log(setTimeout(() => console.log("a"), 1000));
15599(この値は毎回異なります)
---
(1秒後)
a
そうです。setTimeout()
は 即座にタイマーIDを返しますから、これ自体Promiseではありません。
awaitにPromise以外を待たせようとしても待ってくれない
Promiseの確定した値を取り出すために、PromiseインスタンスがFulfilledやRejectedになるのを待つawaitですが、Promiseではなく、ほかの値を渡すとどうなるでしょうか。
それが、先ほどの例になります。
const asyncFunction = async() => {
await setTimeout(() => console.log("Please wait"), 1000)
console.log("Cannot wait");
}
console.log("start");
asyncFunction();
console.log("finish");
start
finish
Cannot wait
---
(1秒後)
Please wait
そう、awaitは右にある値がPromiseインスタンスではない場合、即座に値を取り出してしまいます。
そして、setTimeout()
は実行されると即座にタイマーIDを返すんでしたよね。
よって、setTimeout()
をawaitすると、awaitはタイマーIDをsetTimeout()
からの値だと判断して評価を終了し、もう終わった処理扱いしてしまうのです。
そして、thisISBad.jsのようにほかの処理がない場合、await以降の処理の開始がsetTimeout()
のタイマーの終了よりも早くなってしまうのです。
それでも待たせたいなら
setTimeout()
をawaitで待ってから処理を実行したいケースというのはあまり考えられませんが、それでもPromise以外のものをawaitで待ちたいことはあるかもしれません。
そこで用いられるのが、Promisificationという方法です。
一言でいえば、Promiseインスタンスでラップしてしまうということです。
const promisifiedTimer = (delay) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Please wait")
resolve()
}, delay)
})
}
const asyncFunction = async() => {
await promisifiedTimer(1000)
console.log("I can wait");
}
console.log("start");
asyncFunction();
console.log("finish");
こうすれば、promisifiedTimer
がPromiseインスタンスを返すようになるので、awaitがきちんとSettledになるまで待ってくれます。よって、promisifiedTimer
内でやりたい操作を行ってからresolveするようにすれば、思い通りの実行順になるというわけです。