Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

最後に

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

frameair
情報工学を学んでいました。年齢は若くは無いです。 昔は派遣でエンジニアの真似事(LSI設計、工場の品質管理等)をしていました。 ある時発作で倒れて、2年半ほど引きこもり。 その後、就労継続支援事業所に通い、今は障がい者雇用で役所の臨時職員の仕事をしています。 精神科に通院中。広汎性発達障害、双極性障害、だそうです。
https://frameair.github.io/myprofile/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away