171

次の形式のイベント ループを実行しています。

var i;
var j = 10;
for (i = 0; i < j; i++) {

    asynchronousProcess(callbackFunction() {
        alert(i);
    });
}

0 から 10 までの数字を示す一連のアラートを表示しようとしています。問題は、コールバック関数がトリガーされるまでに、ループがすでに数回繰り返されており、より高い の値が表示されていることですi。これを修正する方法に関する推奨事項はありますか?

4

6 に答える 6

292

すべてのfor非同期操作が開始されている間、ループはすぐに完了するまで実行されます。将来、それらが完了してコールバックを呼び出すと、ループ インデックス変数iの値はすべてのコールバックの最後の値になります。

これは、forループが非同期操作の完了を待たずにループの次の繰り返しに進むためであり、非同期コールバックは後で呼び出されるためです。したがって、ループは反復を完了し、それらの非同期操作が終了すると、コールバックが呼び出されます。そのため、ループ インデックスは「完了」し、すべてのコールバックの最終値にとどまります。

これを回避するには、コールバックごとに個別にループ インデックスを一意に保存する必要があります。Javascript でこれを行う方法は、それを関数クロージャでキャプチャすることです。これは、この目的専用のインライン関数クロージャーを作成することで実行できます (最初の例を以下に示します)。または、インデックスを渡す外部関数を作成して、インデックスを一意に維持できるようにすることもできます (2 番目の例を以下に示します)。

2016 年の時点で、完全に仕様に準拠した JavaScript の ES6 実装を使用している場合は、ループ変数letを定義するために使用することもでき、forループの反復ごとに一意に定義されforます (以下の 3 番目の実装)。ただし、これは ES6 実装の後期実装機能であるため、実行環境がそのオプションをサポートしていることを確認する必要があります。

独自の関数クロージャーを作成するため、.forEach() を使用して反復します

someArray.forEach(function(item, i) {
    asynchronousProcess(function(item) {
        console.log(i);
    });
});

IIFE を使用して独自の関数クロージャを作成する

var j = 10;
for (var i = 0; i < j; i++) {
    (function(cntr) {
        // here the value of i was passed into as the argument cntr
        // and will be captured in this function closure so each
        // iteration of the loop can have it's own value
        asynchronousProcess(function() {
            console.log(cntr);
        });
    })(i);
}

外部関数を作成または変更して変数に渡す

関数を変更できる場合はasynchronousProcess()、そこに値を渡すだけで、次のようにasynchronousProcess()関数を cntr コールバックに戻すことができます。

var j = 10;
for (var i = 0; i < j; i++) {
    asynchronousProcess(i, function(cntr) {
        console.log(cntr);
    });
}

ES6を使用let

ES6 を完全にサポートする Javascript 実行環境がある場合は、次のようにループで使用できletます。for

const j = 10;
for (let i = 0; i < j; i++) {
    asynchronousProcess(function() {
        console.log(i);
    });
}

letこのようにループ宣言で宣言すると、ループの呼び出しごとforに for の一意の値が作成さiれます (これが必要です)。

promise と async/await を使用したシリアル化

非同期関数が promise を返し、非同期操作を並列ではなく次々に実行するようにシリアル化し、 and をサポートする最新の環境で実行しているasync場合awaitは、さらに多くのオプションがあります。

async function someFunction() {
    const j = 10;
    for (let i = 0; i < j; i++) {
        // wait for the promise to resolve before advancing the for loop
        await asynchronousProcess();
        console.log(i);
    }
}

これにより、 への呼び出しが一度に 1 つだけ実行asynchronousProcess()され、forそれぞれが完了するまでループが進行しないことが保証されます。これは、非同期操作をすべて並行して実行する以前のスキームとは異なるため、必要な設計に完全に依存します。注: awaitpromise で動作するため、関数は、非同期操作が完了したときに解決/拒否される promise を返す必要があります。また、 を使用するawaitには、含まれている関数を宣言する必要があることに注意してくださいasync

非同期操作を並行して実行Promise.all()し、結果を順番に収集するために使用します

 function someFunction() {
     let promises = [];
     for (let i = 0; i < 10; i++) {
          promises.push(asynchonousProcessThatReturnsPromise());
     }
     return Promise.all(promises);
 }

 someFunction().then(results => {
     // array of results in order here
     console.log(results);
 }).catch(err => {
     console.log(err);
 });
于 2012-07-14T23:18:05.610 に答える
11

これを修正する方法に関する推奨事項はありますか?

いくつかの。bindを使用できます:

for (i = 0; i < j; i++) {
    asycronouseProcess(function (i) {
        alert(i);
    }.bind(null, i));
}

または、ブラウザがlet (次の ECMAScript バージョンに含まれる予定ですが、Firefox はしばらく前からサポートしています) をサポートしている場合は、次のようにすることができます。

for (i = 0; i < j; i++) {
    let k = i;
    asycronouseProcess(function() {
        alert(k);
    });
}

または、手動でジョブを実行することもできますbind(ブラウザーがサポートしていない場合は、上記のリンクにあるはずの shim を実装できると思います)。

for (i = 0; i < j; i++) {
    asycronouseProcess(function(i) {
        return function () {
            alert(i)
        }
    }(i));
}

私は通常let、それを使用できる場合を好みます (たとえば、Firefox アドオンの場合)。それ以外の場合bind、またはカスタムカリー化関数 (コンテキスト オブジェクトを必要としない)。

于 2012-07-14T23:59:46.183 に答える
2

JavaScript コードは単一のスレッドで実行されるため、ページのユーザビリティに深刻な影響を与えることなく、次のループを開始する前に最初のループの反復が完了するのを待つようにブロックすることは原則としてできません。

解決策は、本当に必要なものによって異なります。例が必要なものに正確に近い場合、i非同期プロセスに渡すという@Simonの提案は良いものです。

于 2012-07-14T23:11:02.873 に答える