#はじめに、の前に(2020/10/29 追記)
(いつの間にか)MDNに、forEachはPromiseを待たない、という趣旨の説明が書かれています。
MDNを読めば、この記事の内容を読む必要は無いと思います……。
#はじめに
Javascriptでは、async/awaitを使う事によって、非同期処理が非常に簡潔に書ける様になっています。
今後はasync/awaitを使ったプログラミングが増えていくと思います。
node.jsで、async/awaitを使ったプログラミングをしていたのですが、ハマった事があったので、自分用の備忘録としてメモ書きです。
#実行環境
OS: Debian 9.6
node.js: v11.2.0
#問題となったコード
配列をconsole.logで出力するだけのプログラムです。
ただし、console.log(value)の前に、sleep(1000)が入っていて、1秒間sleepする様にしています。
このプログラムは、数値が1秒毎に出力され、5秒掛けて全てが出力される事を、意図していました。
'use strict';
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
(async () => {
const a = [1,2,3,4,5];
a.forEach(async (value) => { // → await a.forEach(... と書いても結果は同じです。
await sleep(1000);
console.log(value)
});
})();
出力結果は、下記の通りになります。
この場合、1秒経過後、一瞬で全ての数字が出力されます。
# 下記の数列は、一瞬で表示されます
1
2
3
4
5
意図していたのは、5秒掛けて1つずつ数字が出力される事でしたので、このままだと問題があります。
#forEach - Polyfill
MDNウェブドキュメントで、forEachのPolyfillを調べてみました。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill
下記がそのコードです。(コメントやコードを、一部省略しています)
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback/*, thisArg*/) {
// 一部省略
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
callback.call(T, kValue, k, O); // ←awaitが付いていない
}
k++;
}
};
}
重要なのは、
- Array.prototype.forEachは、async functionでは無い。
- callback.callにawaitが付いていない。
という点です。
その為、callback.callで処理が一時停止する事はありません。
#sample修正版
その為、sampleのコードは、下記の様に書く必要があります。
下記の様に書くと、1秒ごとに数字が出力されました。
'use strict';
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
(async () => {
const a = [1,2,3,4,5];
for(let i of a) {
await sleep(1000);
console.log(i);
}
})();
この様な場合は、forEachなんて使わずに、for-of文を使いなさい、と言う事が解りました。
自分でasyc/awaitに対応したforEachを作るのも良いかも知れません。
今なら、array-foreach-asyncというライブラリがあるとの情報を、コメントにてご指摘を頂きました。
補足
以前は、修正例を下記コードで紹介していました。このコードでは、for-of文を使わず、for文を使っています。
ですが、for-of文を使う方が良いかと思います。
'use strict';
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
(async () => {
const a = [1,2,3,4,5];
for(let i = 0; i < a.length; i++) {
await sleep(1000);
console.log(a[i]);
}
})();
余談 - Promise.all
1回づつ処理を待たずに、全て並列に動かしたい場合は、Promise.allを使用すると良いと思います。
Promise.allについては、コメントにてご指摘を頂きました。
一応、私も適当にサンプルを書いてみました。
解りづらければごめんなさい。
(補足1は、Windows環境、nodejs version: 11.3.0で確認しています。)
'use strict';
// Function - sleep_and_print
function sleep_and_print(max_time, text) {
// max_timeを上限に、ランダムな時間だけsleepする
// sleep後、Textとsleep時間を表示する。
return new Promise((resolve, reject) => {
const wait = Math.random() * max_time
setTimeout(() => {
console.log("Text: " + text + ", Wait: " + wait / 1000.0);
resolve();
}, wait);
});
}
// Main
(async () => {
//----------------------------------------------------
// 処理1 - Promise.all
//----------------------------------------------------
console.log("----- Promise.all -----");
let start = Date.now(); // 処理時間の測定用
// 表示させるテキスト
let a = ["A","B","C","D","E"];
// Promiseの配列を生成
const b = a.map(item => sleep_and_print(5000, item));
// 配列bに入っている処理がすべて終わるまで、await
// 並列処理を行う。
await Promise.all(b);
// 処理は5秒以内に終わる、はず
let stop = Date.now(); // 処理時間の測定用
let result = stop - start;
console.log("Time: " + result / 1000.0); // 処理時間表示
//----------------------------------------------------
// 処理2 - for-of
//----------------------------------------------------
console.log("----- for-of -----");
start = Date.now(); // 処理時間の測定用
// 表示させるテキスト
a = ["A","B","C","D","E"];
for(let i of a) {
await sleep_and_print(5000, i);
}
// 処理は5秒以上かかる、はず
stop = Date.now(); // 処理時間の測定用
result = stop - start;
console.log("Time: " + result / 1000.0); // 処理時間表示
})();
下記が実行結果です。
Waitと書かれた部分が、その処理で行われたsleepの時間、最後のTimeが全ての処理に掛かった時間です。
Promise.allだと、並列実行のため、最悪でも5sec+数msec以内に処理が終了します。
また、並列実行のため、処理が終了する順序はランダムです。
for-ofだと、全ての処理を逐次実行するため、平均で12.5sec程掛かります。
処理は、順序通りに実行されます。
----- Promise.all -----
Text: B, Wait: 3.140756979867685
Text: C, Wait: 3.8320302876606918
Text: A, Wait: 4.607162449097081
Text: E, Wait: 4.875497964313306
Text: D, Wait: 4.996697681841334
Time: 5.003
----- for-of -----
Text: A, Wait: 4.342562029541116
Text: B, Wait: 4.270371805765942
Text: C, Wait: 3.542954967565629
Text: D, Wait: 4.503098764260032
Text: E, Wait: 1.1583077650003248
Time: 17.84
#最後に
async/awaitに対応したforEachを探してみたのですが、私が調べた限りでは見つかりませんでした。
標準のjavascriptには無いのでしょうか。
ただし、ライブラリは存在するとの事なので、それを使ってみるのが良いかもしれません。
コメントで皆様に色々と情報を頂きましたので、そちらも参照してみて下さい。
頂いたコメントの内容の一部を、記事にフィードバックさせて頂きました。(2018/12/06)
情報を頂いた皆様に感謝申し上げます。本当にありがとうございました。
#参考リンク
MDNウェブドキュメント - forEach Polyfill
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill
JavaScript: async/await with forEach()
https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404