JavaScriptでループ中にスリープしたい。それも読みやすいコードでという5年くらい前の記事を読み、コメント欄に、今だったらasync/awaitを使ってこうできるよーという例が挙げあられていたんですが、パッと見て理解できなかったのでメモとして残しておきます。
理解するのに役立ったのは、JavaScript Promiseの本の1.2.1. Promise workflow、5.3. await式です。
setTimeoutの挙動をgifで説明している♻️ JavaScript Visualized: Event Loopも参考になりました。
今回は、1000ミリ秒ごとにランダムな数字を出力する処理を10回繰り返すコードを目標とします。
以下は、動かないけど実現したいコードです。
for (let i = 0; i < 10; i++) {
console.log(Math.random())
sleep(1000);
}
async/awaitでやってみる
上のコードは、以下のように async/await を用いて実現できます。
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
async function main() {
for (let i = 0; i < 10; i++) {
console.log(Math.random());
await sleep(1000);
}
}
main()
ちょっとした解説
-
await式では非同期処理を実行し完了するまで、次の行(次の文)を実行しない
=>sleep(1000)
の実行が終わるまでfor文の次の周期にはいかない(1000ミリ秒停止する)。 -
sleep関数
はPromiseを返している
=>sleep(1000)
の実行が終わる = PromiseがFulfilledになる = promiseオブジェクトがsetTimeoutでmilliseconds(1000ミリ秒)後にresolveされる
new Promise(resolve => setTimeout(resolve, milliseconds));
ってなんだ?
違和感があったのは、await式の返り値がないことです。
例えば、以下のコードだとawait式の返り値は sccess となります。
function sleep(milliseconds) {
return new Promise(resolve => {
setTimeout(() => {
resolve("success")
}, milliseconds)
});
}
async function main() {
for (let i = 0; i < 10; i++) {
console.log(Math.random());
let value = await sleep(1000);
console.log(value) // sccess
}
}
main()
しかし今回は時間を止めるだけなのでPromiseは値をresolveする必要はないため、ちょっと違和感のあるコードになっていました。
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
async function main() {
for (var i = 0; i < 10; i++) {
console.log(Math.random());
let value = await sleep(1000);
console.log(value) // undefined
}
}
main()
async/awaitを使わずPromiseだけでやってみる
特に意味はないですが勉強になりそうなので。
Promiseのthenを復習する
100の2倍を求める非同期関数の使用例です。
function func1(data, callback) {
setTimeout(() => {
callback(data * 2);
}, 1000);
}
function sample_callback() {
func1(100, function(value) {
console.log(value); //200
});
}
sample_callback();
この時点でつまずきそうですが、関数そのものを引数にして渡してるだけです。
これをPromiseで書き換えると以下になります。
function func2(data) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(data * 2);
}, 1000);
});
}
function sample_promise() {
func2(100).then(data => {
console.log(data); // 200
});
}
sample_promise();
Promiseが1000ミリ秒後に resolve(data * 2)
と解決されて then の onFulfilled に設定された関数に data * 2
という値を渡します。
awaitの代わりにthenを使っているイメージを持っているのですがあってるでしょうか???
promise chain
以下のコードでは、promise chain をつなげて処理をしています。
returnされてきた func2(data)
に対してさらに then で処理することを繰り返しています。
このコードでは、一秒間隔で 200, 400, 800 が出力されます。
function func2(data) {
return new Promise(function(resolve) {
setTimeout(() => {
resolve(data * 2);
}, 1000);
});
}
function sample_promise3() {
func2(100)
.then(data => {
console.log(data);
return func2(data); // 200
})
.then(data => {
console.log(data);
return func2(data); // 400
})
.then(data => {
console.log(data); // 800
});
}
sample_promise3();
本題
ここまで理解できたのでasync/awaitを使わずPromiseだけでやってみます。
let myPromise = main(1000);
for (let i = 0; i < 3; i++) {
myPromise = myPromise.then(num => {
console.log(num);
return main(1000);
});
}
function main(milliseconds) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(Math.random());
}, milliseconds);
});
}
for文の中身が少しややこしくて、
for (var i = 0; i < 3; i++) {
myPromise = myPromise.then(num => {
console.log(num);
return main(1000);
});
}
は、以下と(ほぼ)同義です。
myPromise
.then(num => {
console.log(num);
return main(1000);
})
.then(num => {
console.log(num);
return main(1000);
})
.then(num => {
console.log(num);
});
もっと分かりやすくすると、
for (var i = 0; i < 3; i++) {
myPromise = myPromise.then(hoge)
}
は、以下と同義です。
myPromise
.then(hoge)
.then(hoge)
.then(hoge);
まとめ
Promiseのみを使う
let myPromise = main(1000);
for (let i = 0; i < 3; i++) {
myPromise = myPromise.then(num => {
console.log(num);
return main(1000);
});
}
function main(milliseconds) {
return new Promise(function(resolve) {
setTimeout(() => {
resolve(Math.random());
}, milliseconds);
});
}
async/awaitを使う
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
async function main() {
for (let i = 0; i < 3; i++) {
console.log(Math.random());
await sleep(1000);
}
}
main()
async/awaitの方がだいぶ直感的ですね!!
「Promiseのみを使う」のlet myPromise = main(1000);
で最初にpending状態のPromiseを定義するのも違和感がありますね。
半日かけでじっくりやったので、Promiseおよびasync/awaitが分かった気になってます。
間違いがあればご指摘お願いします!!