web-dev-qa-db-ja.com

JavaScript ES6はループの約束をする

for (let i = 0; i < 10; i++) {
    const promise = new Promise((resolve, reject) => {
        const timeout = Math.random() * 1000;
        setTimeout(() => {
            console.log(i);
        }, timeout);
    });

    // TODO: Chain this promise to the previous one (maybe without having it running?)
}

上記の結果、次のようなランダムな出力が得られます。

6
9
4
8
5
1
7
2
3
0

作業は簡単です。それぞれの約束がもう一方の約束の後にのみ実行されるようにします(.then())。

どういうわけか、私はそれをする方法を見つけることができませんでした。

ジェネレータ関数(yield)を試し、約束を返す単純な関数を試しましたが、結局のところ同じ問題に陥ります。ループは同期的です

async では、async.series()を使います。

どのように解決しますか?

88
Poni

あなたがすでにあなたの質問で示唆したように、あなたのコードはすべての約束を同期的に作ります。代わりに、それらは前の1つが解決するときにのみ作成されるべきです。

次に、new Promiseで作成された各約束は、resolve(またはreject)への呼び出しで解決される必要があります。これはタイマーが切れるとき行われるべきです。それはあなたがその約束で持っているであろうどんなthenコールバックも引き起こすでしょう。そしてそのようなthenコールバック(またはawait)はチェーンを実装するために必要です。

これらの要素を使って、この非同期連鎖を実行する方法がいくつかあります。

  1. すぐに解決できる約束で始まるforループ

  2. Array#reduceでは、すぐに解決できる約束から始まります。

  3. 自分自身を解決コールバックとして渡す関数を使って

  4. ECMAScript2017の async/await構文を使って

  5. ECMAScript2020で提案されている for await...of構文

下記の各オプションについては、スニペットとコメントを参照してください。

1. forとは

あなたforループを使うことができますことができますが、それがnew Promiseを同期的に実行しないことを確認しなければなりません。代わりに、すぐに解決する最初の約束を作成してから、前の約束が解決するように新しい約束を連鎖させます。

for (let i = 0, p = Promise.resolve(); i < 10; i++) {
    p = p.then(_ => new Promise(resolve =>
        setTimeout(function () {
            console.log(i);
            resolve();
        }, Math.random() * 1000)
    ));
}

2. reduceとは

これは、以前の戦略に対するより機能的なアプローチです。実行したいチェインと同じ長さの配列を作成し、すぐに解決できる約束から始めます。

[...Array(10)].reduce( (p, _, i) => 
    p.then(_ => new Promise(resolve =>
        setTimeout(function () {
            console.log(i);
            resolve();
        }, Math.random() * 1000)
    ))
, Promise.resolve() );

これは、実際に約束で使用されるデータを含む配列を持つ(---)場合には、おそらくもっと便利です。

3.自分自身を解決コールバックとして渡す関数

ここで関数を作成してすぐに呼び出します。それは最初の約束を同期的に作ります。それが解決すると、関数が再び呼び出されます。

(function loop(i) {
    if (i < 10) new Promise((resolve, reject) => {
        setTimeout( () => {
            console.log(i);
            resolve();
        }, Math.random() * 1000);
    }).then(loop.bind(null, i+1));
})(0);

これはloopという名前の関数を作成し、コードの最後にそれが引数0で即座に呼び出されるのを見ることができます。これはカウンタであり、i引数です。そのカウンターがまだ10未満の場合、この関数は新しい約束を作成します。それ以外の場合、連鎖は停止します。

resolve()への呼び出しはthenコールバックを引き起こし、これは関数を再び呼び出します。 loop.bind(null, i+1)_ => loop(i+1)の別の言い方です。

4. async/awaitの場合

最近のJSエンジン はこの構文をサポートしています

(async function loop() {
    for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
        console.log(i);
    }
})();

new Promise()呼び出しが同期的に実行されるようにに見えるので奇妙に見えるかもしれませんが、実際にはasync関数は実行時にを返します。最初のawait。約束された約束が解決されるたびに、関数の実行中のコンテキストは復元され、awaitの後、次のコンテキストに遭遇するまで進み、ループが終了するまで続きます。

タイムアウトに基づいて約束を返すのは一般的なことかもしれないので、そのような約束を生成するための別の関数を作成することができます。これは関数を約束すると呼ばれ、この場合はsetTimeoutです。コードの読みやすさが向上する可能性があります。

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

(async function loop() {
    for (let i = 0; i < 10; i++) {
        await delay(Math.random() * 1000);
        console.log(i);
    }
})();

5. for await...ofを使って

さらに最近では、 for await...of 構文が、一部のJavaScriptエンジンにも採用されています。この場合、実際にはコードが削減されるわけではありませんが、実際の反復からランダムな区間チェーンの定義を分離することができます。

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function * randomDelays(count ,max) {
    for (let i = 0; i < count; i++) yield delay(Math.random() * max).then(() => i);
}

(async function loop() {
    for await (let i of randomDelays(10, 1000)) console.log(i);
})();
201
trincot

これにはasync/awaitを使用できます。もっと説明しますが、実際には何もありません。これは単なる通常のforループですが、私はあなたのPromiseの構築の前にawaitキーワードを追加しました

これについて私が気に入っているのは、あなたのPromiseがあなたのコード(あるいは他の答え)のような副作用を持つ代わりに通常の値を解決できることです。これは、のような力を与えます。ゼルダの伝説:過去へのリンクLight Worldの両方に影響を与えることができる場所およびthe Dark World - つまり、Promisedデータが利用可能になる前後で、深くネストされた関数、その他の扱いにくい制御構造体、または愚かな IIFE s。

// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld

だからここにそれがどのようになるのでしょうか...

const someProcedure = async n =>
  {
    for (let i = 0; i < n; i++) {
      const t = Math.random() * 1000
      const x = await new Promise(r => setTimeout(r, t, i))
      console.log (i, x)
    }
    return 'done'
  }

someProcedure(10).then(x => console.log(x)) // => Promise
// 0 0
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5
// 6 6
// 7 7
// 8 8
// 9 9
// done

私たちの手順の中で、厄介な.then呼び出しに対処する必要がないのを見てください。そしてasyncキーワードは自動的にPromiseが返されることを確実にするので、返された値で.then呼び出しをチェーニングすることができます。これは大成功のための準備です:n Promise、の順に実行してからを実行してください - 成功/エラーメッセージの表示のように。

10
user633183

Trincotによる優れた答えに基づいて、配列内の各項目を処理するハンドラを受け入れる再利用可能な関数を書きました。関数自体が、ループが終了するまで待つことを可能にする約束を返し、あなたが渡すハンドラ関数もまた約束を返すかもしれません。

loop(items、handler):約束

それを正しくするには少し時間がかかりましたが、次のコードは多くの約束ループの状況で使用可能になると思います。

コピー&ペースト可能コード:

// SEE https://stackoverflow.com/a/46295049/286685
const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

使用法

これを使用するには、最初の引数としてループオーバーする配列と2番目のハンドラー関数で呼び出します。 3番目、4番目、5番目の引数にパラメーターを渡さないでください。これらは内部的に使用されます。

const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

const items = ['one', 'two', 'three']

loop(items, item => {
  console.info(item)
})
.then(() => console.info('Done!'))

高度なユースケース

ハンドラ関数、ネストループ、エラー処理を見てみましょう。

ハンドラー(current、index、all)

ハンドラは3つの引数を渡されます。現在のアイテム、現在のアイテムのインデックス、およびループ全体の配列。ハンドラー関数が非同期作業を行う必要がある場合は、約束を返すことができ、ループ関数は約束が解決するのを待ってから次の反復を開始します。あなたはループ呼び出しを入れ子にすることができ、すべてが期待通りに動作します。

const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

const tests = [
  [],
  ['one', 'two'],
  ['A', 'B', 'C']
]

loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
  console.info('Performing test ' + idx)
  return loop(test, (testCase) => {
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)
}))
.then(() => console.info('All tests done'))

エラー処理

私が見た多くの約束ループの例は、例外が発生したときに故障します。この機能を正しく動作させるのはかなりトリッキーでしたが、私が言うことができる限りではそれは現在働いています。キャッチハンドラを必ず内側のループに追加し、それが発生したときには棄却関数を呼び出してください。例えば。:

const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

const tests = [
  [],
  ['one', 'two'],
  ['A', 'B', 'C']
]

loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
  console.info('Performing test ' + idx)
  loop(test, (testCase) => {
    if (idx == 2) throw new Error()
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)  //  <--- DON'T FORGET!!
}))
.then(() => console.error('Oops, test should have failed'))
.catch(e => console.info('Succesfully caught error: ', e))
.then(() => console.info('All tests done'))

アップデート:NPMパッケージ

この答えを書いて以来、私は上記のコードをNPMパッケージに入れました。

for-async

インストール

npm install --save for-async

インポート

var forAsync = require('for-async');  // Common JS, or
import forAsync from 'for-async';

使用法(非同期)

var arr = ['some', 'cool', 'array'];
forAsync(arr, function(item, idx){
  return new Promise(function(resolve){
    setTimeout(function(){
      console.info(item, idx);
      // Logs 3 lines: `some 0`, `cool 1`, `array 2`
      resolve(); // <-- signals that this iteration is complete
    }, 25); // delay 25 ms to make async
  })
})

詳細はパッケージのreadmeを見てください。

3
Stijn de Witt

あなたがES6に制限されている場合、最善の選択肢はすべて約束です。 Promise.all(array)はまた、array引数内のすべての約束を正常に実行した後で、一連の約束を返します。データベース内の多数の学生レコードを更新したい場合、次のコードはPromise.allの概念を示しています。

let promises = [];
students.map((student, index) => {
  student.rollNo = index + 1;
  student.city = 'City Name';
  //Update whatever information on student you want
  promises.Push(student.save());
  //where save() is a function used to save data in mongoDB
});
Promise.all(promises).then(() => {
  //All the save queries will be executed when .then is executed
  //You can do further operations here after as all update operations are completed now
});

Mapはループの単なるメソッドの例です。 forforin、またはforEachループを使用することもできます。だから概念は非常に簡単です、あなたが一括非同期操作を行いたいループを開始します。そのようなすべての非同期操作ステートメントを、そのループの有効範囲外で宣言された配列にプッシュします。ループが完了したら、そのようなquery/promiseの準備された配列を引数としてPromise allステートメントを実行します。

基本的な概念は、javascriptループは同期的であるのに対し、データベース呼び出しは非同期的であり、またプッシュ方式を同期的に使用することです。そのため、ループ内では非同期動作の問題は発生しません。

1
Srk95

これが私の2セントの価値です:

  • 調整可能な関数forpromise()
  • forループの古典をエミュレートします
  • 値を返す内部ロジックに基づく早期終了を可能にします
  • resolve/next/collectに渡された結果の配列を収集できます
  • デフォルトはstart = 0、increment = 1です。
  • ループの内側でスローされた例外は捕捉され、.catch()に渡されます。
    function forpromise(lo, hi, st, res, fn) {
        if (typeof res === 'function') {
            fn = res;
            res = undefined;
        }
        if (typeof hi === 'function') {
            fn = hi;
            hi = lo;
            lo = 0;
            st = 1;
        }
        if (typeof st === 'function') {
            fn = st;
            st = 1;
        }
        return new Promise(function(resolve, reject) {

            (function loop(i) {
                if (i >= hi) return resolve(res);
                const promise = new Promise(function(nxt, brk) {
                    try {
                        fn(i, nxt, brk);
                    } catch (ouch) {
                        return reject(ouch);
                    }
                });
                promise.
                catch (function(brkres) {
                    hi = lo - st;
                    resolve(brkres)
                }).then(function(el) {
                    if (res) res.Push(el);
                    loop(i + st)
                });
            })(lo);

        });
    }


    //no result returned, just loop from 0 thru 9
    forpromise(0, 10, function(i, next) {
        console.log("iterating:", i);
        next();
    }).then(function() {


        console.log("test result 1", arguments);

        //shortform:no result returned, just loop from 0 thru 4
        forpromise(5, function(i, next) {
            console.log("counting:", i);
            next();
        }).then(function() {

            console.log("test result 2", arguments);



            //collect result array, even numbers only
            forpromise(0, 10, 2, [], function(i, collect) {
                console.log("adding item:", i);
                collect("result-" + i);
            }).then(function() {

                console.log("test result 3", arguments);

                //collect results, even numbers, break loop early with different result
                forpromise(0, 10, 2, [], function(i, collect, break_) {
                    console.log("adding item:", i);
                    if (i === 8) return break_("ending early");
                    collect("result-" + i);
                }).then(function() {

                    console.log("test result 4", arguments);

                    // collect results, but break loop on exception thrown, which we catch
                    forpromise(0, 10, 2, [], function(i, collect, break_) {
                        console.log("adding item:", i);
                        if (i === 4) throw new Error("failure inside loop");
                        collect("result-" + i);
                    }).then(function() {

                        console.log("test result 5", arguments);

                    }).
                    catch (function(err) {

                        console.log("caught in test 5:[Error ", err.message, "]");

                    });

                });

            });


        });



    });
0
cestmoi