##async/awaitとは
非同期の処理を同期的に書くために使うことは大体の人が知っていると思います。
しかしそれをいざ使ってと言われると私のような初心者は困ってしまいます。
そんな人達にのためにこの記事を書きました。
カウントダウン処理のコードを書きながらawait/asyncの使い方を学んでいきます。
##したいこと(目標)
カウントダウン処理ををawait/asyncを使って実装します。
具体的には以下のようにコンソールで表示できるようにします。
start, 5, 4, 3, 2, 1, next
##プラン
これからやることは
- start関数を作る
- next関数を作る
- countdown関数を作る
- それぞれが意図したとおりの順番に動くように調整する
です。
##start関数とnext関数を作る
コンソールに文字を表示するだけなので、とても簡単です。
console.log("start");
//この間にカウントダウンの関数を入れたい
console.log("next");
このままだとイマイチなので関数として呼び出せるようにします。
//all関数を実行する
all();
//goとnextの関数をまとる関数を作る
function all() {
start();
//この間にカウントダウンの関数を入れたい
next();
}
function start() {
console.log("start");
}
function next() {
console.log("next");
}
##カウントダウン関数を作る
###とりあえずsetTimeoutを使って見る
今回はsetTimeoutを使用してつくります。
とりあえず1秒後にカウントダウンとコンソールで表示されるようにします。
function countdown() {
setTimeout(() => {
console.log("countdown");
}, 1000)
}
今のままでは結果が以下のようになります。
"start"
"next"
"countdown"
###async/awaitを使って同期的な処理にする
start, countdown, nextとなって欲しいのでここでようやくasync/awaitを使います。
async function all() {
start();
await countdown();
next();
}
###async/awaitはpromiseが返ってくる必要がある!
しかし、これではまだ駄目です。
単純にasync/awaitをつければいいわけではありません。(私はそう思っていました。)
async/awaitを使うにはpromiseが返ってくるようにする必要があります。
そのために、度のタイミングでpromiseが返ってくるかをcountdown関数に書き込みます。
function countdown() {
return new Promise(resolve => {
setTimeout(() => {
console.log("countdown");
resolve() //1秒後に”countdown”が出たらresolve状態になるという合図
}, 1000)
})
}
これでコンソールに正しい順番で表示されます。
"start"
"countdown"
"next"
Promiseとasyncの関係はこちらを参照してください。
###カウントダウンされるようにコードを書く
続いて4,3,2,1,0とカウントダウンされるように処理します。
ここからは少しややこしくなります。
all();
async function all() {
start();
await countdown(5); //secの初期値を5にしておく
next();
}
function start() {
console.log("start");
}
function next() {
console.log("next");
}
function countdown(sec) {
return new Promise(resolve => {
console.log(sec)
sec -= 1 //secの値を1ずつ減らしていく
setTimeout(() => {
countdown(sec); //1秒毎にcountdownが何度も呼び出される処理
resolve()
}, 1000)
})
}
こうすると値は5,4,3,2,1となっていきます。
###clearTimeoutを使ってsetTimeoutの処理を止める
しかし、これでは0以下にもどんどんと値が続いてしまうのでclearTimeoutを使ってsecの値が0になったら処理が止まるようにします。
function countdown(sec) {
return new Promise(resolve => {
console.log(sec)
sec -= 1
const timeId = setTimeout(() => {
countdown(sec);
resolve();
}, 1000)
if(sec === 0) { //secの値が0になったら処理
clearTimeout(timeId);
}
})
}
0で止まるようにありましたが、このままでは以下の通りカウントダウンの途中で次の処理が行われてしまいます。
"start"
5
4
"next"
3
2
1
###再びasync/awaitの登場
ここでの問題はcountdown関数が一度回ったあと(secの値が5から4に変わる時)next関数が実行されることにあるようです。
setTimeoutは非同期を行うための処理です。なのでsetTimeoutを同期的な処理に変える必要があります。そのためにここでまたasync/awaitが必要になってきます。
function countdown(sec) {
return new Promise(resolve => {
console.log(sec);
sec -= 1;
const timeId = setTimeout(async () => {
await countdown(sec);
resolve();
}, 1000)
if (sec === 0) {
clearTimeout(timeId);
}
})
}
とするとsetTimeoutの処理を一回ごとに同期的な処理にしてくれます。(1秒ごとにresolveが返ってくるまで待つということ)
コンソールでは以下のように表示されます。
"start"
5
4
3
2
1
今度はカウントが1まで言っているのにnext関数が呼び出されていません。
###resolveが呼び出されないと次の処理にはいけない!
これはsecが0になったときにresolveが返っていないので、次の処理が行われないためです。
なのでresolveを追加すればOKのハズです。
function countdown(sec) {
return new Promise(resolve => {
console.log(sec)
sec -= 1
const timeId = setTimeout(async () => {
await countdown(sec);
resolve();
}, 1000)
if (sec === 0) {
clearTimeout(timeId);
resolve(); ///このタイミングでresolveがかえるようにする
}
})
}
こうすることで意図したとおりの動作をしてくれます。
"start"
5
4
3
2
1
"next"
##まとめ
ながながと解説してきましたが、これで終わりです。
単純なカウントダウンを作るだけなら特に困らなかったと思いますが、それを前後別の動作と組み合わせるとなかなかややこしいなと思ったのでまとめてみました。
ポイントは"async/awaitだけではなくresolveが返ってくるタイミングを考える"です。
非同期の処理を同期的な処理にするにはasync/awaitが必要ですが、それだけではなく、いつまで処理を待てばいいのかを教える「resolveを返す」という処理をセットで考える必要があります。
特にsetTimeoutのなかにasync/awaitを使うという発想がなかったので備忘録を兼ねて記事にしてみました。
皆さんのお役に立てば幸いです