質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.40%

  • JavaScript

    18112questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • Node.js

    2084questions

    Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

  • ECMAScript

    128questions

    ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

  • 非同期処理

    117questions

    非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

  • Redis

    114questions

    Redisは、オープンソースのkey-valueデータストアで、NoSQLに分類されます。すべてのデータをメモリ上に保存するため、処理が極めて高速です。

js array forEach内で走る非同期処理の終了タイミングをひろう方法について

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 6,081

bleurouge

score 153

配列をforEachでまわし、内側で走る非同期処理がすべて終了したタイミングをひろい、次の処理に続けたい場合どのような方法があるでしょうか?node.js で keyの配列をforEachでまわし、内側でredis-client.get()を利用するケースを想定しています。

下記の方法で目的の動作はできているのですが、何かスマートじゃないような...
そもそも、forEach?また、まだ利用できていないのですが、generatorを使って次の処理をsleepしておく方法?などもありそうな気がしています。他に良い方法があれば教えていただければ幸いです。よろしくお願いします。

var keysArr = ['a','b','c','d','e','f','g','h','i','j','k','l'];
var forEachWithAsync = function(arr) {
  return new Promise(function(resolve) {
    var cnt = 0;
    console.log('length: ' + arr.length);
    arr.forEach(function(val, idx, arr) {
      // node_redis client.get(key, function(err, val) {...}); を使うケースを想定 
      setTimeout(function() {
        ++cnt;
        console.log(cnt + ' async log: ' + val);
        if(cnt === arr.length) { resolve(); }
      }, Math.round(Math.random()*10000));
    });
  });
}

forEachWithAsync(keysArr)
.then(function() { console.log('end'); });
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+4

これの出番です.

せっかくなのでES6の機能ふんだんに使って書きます.
(Node.jsということなので,特にブラウザの互換性のような問題も気にしなくていいと思います)

'use strict';

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const keys = ['a','b','c','d','e','f','g','h','i','j','k','l'];

Promise.all(keys.map(
  value => sleep(Math.round(Math.random()*10000)).then(() => console.log('done: ' + value))
)).then(
  () => console.log('[all done]'), // すべてresolveされた
  e => console.error(e) // ひとつでもrejectされた,または例外がスローされた
);

co+Generatorを使うならもう少し見やすくかけますね.

'use strict';

const co = require('co');

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const keys = ['a','b','c','d','e','f','g','h','i','j','k','l'];

co(function* () {
  yield keys.map(function* (value) {
    yield sleep(Math.round(Math.random()*10000));
    console.log('done: ' + value);
  });
  console.log('[all done]');
}).catch(e => console.error(e));

Babelでコンパイルする前提でasync/awaitを使うなら

'use strict';

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const keys = ['a','b','c','d','e','f','g','h','i','j','k','l'];

(async () => {
  await Promise.all(keys.map(async value => {
    await sleep(Math.round(Math.random()*10000));
    console.log('done: ' + value);
  }));
  console.log('[all done]');
})().catch(e => console.error(e));

/*

coを使うけどよりPromiseやasync/awaitとの親和性を高くして書く場合もこのように

co(function* () {
  yield Promise.all(keys.map(value => co(function* () {
    yield sleep(Math.round(Math.random()*10000));
    console.log('done: ' + value);
  })));
  console.log('[all done]');
}).catch(e => console.error(e));

*/

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/15 21:16

    編集履歴が Array(7).fill('a') なのが地味に気になります…。

    キャンセル

  • 2016/04/15 21:28

    (ごめんなさいGitのコミットメッセージも雑にするタイプなので)

    キャンセル

  • 2016/04/16 00:47

    数パターンも記載いただきありがとうございました!このような関数の組み合わせで目的の処理を作れるところがjavascriptの魅力ですね。mapでpromiseな配列要素を作り直すのは実行速度的には気になるところですが、きっとループ内で非同期処理が発生するケースはI/Oでしょうから杞憂なのでしょう。Promiseとarray.xxxxとの組み合わせで何かできるケースは他にもありそうですが、なかなか発想が追いつかない状態です。参考にさせていただきます!

    キャンセル

+1

個人的には setTimeout() だけで完結させる方法をとります。

 setTimeout

'use strict';
setTimeout(function timeout (array, i, resolve) {
  console.log(i, array[i]);

  if (++i < array.length) {
    setTimeout(timeout, Math.round(Math.random()*10000), array, i, resolve);
  } else {
    resolve(array);
  }
}, Math.round(Math.random()*10000), ['a','b','c','d','e','f','g','h','i','j','k','l'], 0, function resolve (array) { console.log('finished!'); });

 setTimeout Polyfill (for IE9-)

setTimeout の第3引数は IE9- 未対応なので Polyfill を適用させます。

Re: bleurouge さん

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/16 00:50

    参考になるご回答ありがとうございました!並行して走り出さない点がPromise.all()+mapとの違いでしょうが、ケースによっては再帰で配列要素を参照していく方法がよさそうですね。しかし、setTimeoutの再帰はなかなか見応えがw

    キャンセル

  • 2016/04/16 20:44 編集

    > 並行して走り出さない点がPromise.all()+mapとの違いでしょうが
    Promise は並行して走りだしませんよ。
    Promise はコールバック関数による連続処理を扱いやすくするためのインターフェースです。非同期処理を並列処理してくれる魔法の機能ではありませんので…。

    試しにPromiseでコードを書いたらCertaiNさんの書いたコードとほぼ同じものが出来ました。別々の関数を順次処理していくのには便利ですが、同じ関数を連続処理するなら再帰処理の方がスマートだと考えています。

    キャンセル

  • 2016/04/16 21:58

    ありがとうございます。非同期処理が走り始める様子が、並行して走り始めるようにみえるようですね。

    ▼並列処理と、どっかで見た記憶が訂正されていました
    http://ja.stackoverflow.com/questions/10363/

    よくよく考えてみると、promise.all([])のかっこ内は配列で、個々の配列要素を一度に操作する術があれば、そもそもforEachなんていらなくなりますね。

    キャンセル

  • 2016/04/16 22:18

    質問文のforEachに関してはsetTimeoutを10回連続処理した後にconsole.log('end')を呼び出しています。
    setTimeoutは1回目のタイマー処理前に2回目のタイマー処理が来てしまった場合に1回目のタイマー処理が終わるまで待ち、1回目のタイマー処理直後に2回目のタイマー処理が発生します。
    従って、順序性は失われませんが、インターバルをランダムにする機能が期待通りに動作していません。
    forEachは同期処理を順次実行するのには有効ですが、非同期処理を順次実行するならコールバック関数を正しく実装する必要があります。
    コールバック処理を再帰処理にするか、Promiseにするかは状況によりますが…。

    キャンセル

  • 2016/04/17 02:15 編集

    setTimeoutの件ですが、以下私の質問に記載のコードについてコメントいただいた内容かと思います。
    >setTimeoutは1回目のタイマー処理前に2回目のタイマー処理が来てしまった場合に1回目のタイマー処理が終わるまで待ち、1回目のタイマー処理直後に2回目のタイマー処理が発生します。従って、順序性は失われませんが、インターバルをランダムにする機能が期待通りに動作していません。

    これは私の認識で間違っていたら訂正いただきたいのですが、

    1. forEachのループのたび、setTimeoutはイベントループに、第一引数で指定した関数を第二引数指定の時間「Math.round(Math.random()*10000)」とともに登録
    2. イベントループのたび、登録した時間が経過しているかチェックされる
    3. イベントループ上で指定時間「Math.round(Math.random()*10000)」経過しているなら、登録した関数が実行される

    つまり、イベントループに登録した時点で、すでにsetTimeoutの実行順序は関係なくなり、イベントループ上に登録された実行までの指定時間が実行順序を左右する。
    setTimeoutが非同期処理といわれる点でもそのような認識でした。

    その場合、非同期処理を行うsetTimeout関数の第二引数がMath.round(Math.random()*10000)ならば、イベントループ上で他にブロックする処理が入らない限り、おおよそ10秒以内にすべてのconsole.logを表示する動作になるかと思います。

    キャンセル

同じタグがついた質問を見る

  • JavaScript

    18112questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • Node.js

    2084questions

    Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

  • ECMAScript

    128questions

    ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

  • 非同期処理

    117questions

    非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

  • Redis

    114questions

    Redisは、オープンソースのkey-valueデータストアで、NoSQLに分類されます。すべてのデータをメモリ上に保存するため、処理が極めて高速です。