Edited at

async/awaitを、Array.prototype.forEachで使う際の注意点、という話


はじめに

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秒掛けて全てが出力される事を、意図していました。


sample

'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秒経過後、一瞬で全ての数字が出力されます。


output

# 下記の数列は、一瞬で表示されます

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

下記がそのコードです。(コメントやコードを、一部省略しています)


forEach

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秒ごとに数字が出力されました。


sample修正版

'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文を使う方が良いかと思います。


sample修正版-修正前

'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で確認しています。)


補足1

'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程掛かります。

処理は、順序通りに実行されます。


補足1-実行結果

----- 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


余談 - for-inループについて

for-inループを、配列に使うべきではありません。

意図しない結果を招くことがあるので、使用は避けた方が良いとか。


sample2

let a = [1, 2, 3, 4, 5];

// この書き方(for-inループ)は避けたほうが良い。
for(let i in a) {
await sleep(1000);
console.log(a[i]);
}

sample2の例だと、使用しても問題にはなりません。

for-inの使用が問題となる典型的な例として、JQueryが挙げられます。

※sample3は、jsfiddleで確認しました。


sample3-html

<div id="main">

<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</div>


sample3-javascript(JQuery)

var a = $("#main > div");

for (var i = 0; i < a.length; i++) {
console.log("output: " + $(a[i]).text());
}

for (var i in a) {
console.log("index output: " + i);
}



output-console

output: 1

output: 2
output: 3
output: 4
output: 5
index output: 0
index output: 1
index output: 2
index output: 3
index output: 4
index output: length
index output: prevObject
index output: jquery
index output: constructor
// 以下省略…

for-inだと、配列の添字以外の、他のプロパティが出力される為、for-inは使えない、と言う事です。

配列は、下記の様に書いても問題は無い為、配列と連想配列は混在させる事が可能だからです。

// AとBのコードは等価となる。

// A
var array = ['hoge', 'fuga'];

// B
var array = { 0: 'hoge', 1: 'fuga', length: 2 };
array.__proto__ = Array.prototype;

その為、配列等にはfor-of文を使う必要があります。

for-of文はiterableなオブジェクトに対して、反復的なループを行います。


sample2.1

const a = [10,20,30,40,50];

a.test = "hello";
for(let o of a) {
console.log(o); // helloは出力されない。
}


最後に

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

IT戦記:JavaScript の配列と連想配列の違い

http://d.hatena.ne.jp/amachang/20070202/1170386546