次の形式のイベントループを実行しています。
var i;
var j = 10;
for (i = 0; i < j; i++) {
asynchronousProcess(callbackFunction() {
alert(i);
});
}
0から10までの数字を示す一連のアラートを表示しようとしています。問題は、コールバック関数がトリガーされるまでに、ループが既に数回反復され、i
のより高い値が表示されることです。これを修正する方法に関する推奨事項はありますか?
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
は、ループの呼び出しごとにi
の一意の値を作成します(これが必要です)。
promisesおよびasync/awaitでのシリアル化
非同期関数がプロミスを返し、非同期操作をシリアル化して並列ではなく次々に実行し、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);
}
}
これにより、asynchronousProcess()
への呼び出しが一度に1つだけ実行され、for
ループは各呼び出しが完了するまで進みません。これは、非同期操作をすべて並行して実行していた以前のスキームとは異なるため、どの設計が必要かによって異なります。注:await
はpromiseで機能するため、非同期操作が完了すると、関数は解決/拒否されるpromiseを返す必要があります。また、await
を使用するには、包含関数をasync
として宣言する必要があります。
async await
はここ(ES7)にあるので、この種のことを今非常に簡単に行うことができます。
var i;
var j = 10;
for (i = 0; i < j; i++) {
await asycronouseProcess();
alert(i);
}
、これはasycronouseProcess
がPromise
を返している場合にのみ機能することを忘れないでください
asycronouseProcess
がコントロールにない場合、次のように自分でPromise
を返すようにできます。
function asyncProcess() {
return new Promise((resolve, reject) => {
asycronouseProcess(()=>{
resolve();
})
})
}
次に、この行をawait asycronouseProcess();
でawait asyncProcess();
に置き換えます
async await
を調べる前であってもPromises
を理解する必要があります(async await
のサポートについてもお読みください)
これを修正する方法に関する推奨事項はありますか?
いくつか。 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
のジョブを手動で行うこともできます(ブラウザがサポートしていない場合は、その場合は上記のリンクにシムを実装できます)。
for (i = 0; i < j; i++) {
asycronouseProcess(function(i) {
return function () {
alert(i)
}
}(i));
}
私は通常、使用できる場合はlet
を好みます(例:Firefoxアドオン)。それ以外の場合はbind
またはカスタム currying 関数(コンテキストオブジェクトを必要としません)。
var i = 0;
var length = 10;
function for1() {
console.log(i);
for2();
}
function for2() {
if (i == length) {
return false;
}
setTimeout(function() {
i++;
for1();
}, 500);
}
for1();
ここに期待されるものへの機能的なアプローチの例を示します。
ES2017:非同期コードを関数(XHRPostなど)内にラップして、promise(promise内の非同期コード)を返すことができます。
次に、forループ内で、魔法のAwaitキーワードを使用して関数(XHRPost)を呼び出します。 :)
let http = new XMLHttpRequest();
let url = 'http://sumersin/forum.social.json';
function XHRpost(i) {
return new Promise(function(resolve) {
let params = 'id=nobot&%3Aoperation=social%3AcreateForumPost&subject=Demo' + i + '&message=Here%20is%20the%20Demo&_charset_=UTF-8';
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function() {
console.log("Done " + i + "<<<<>>>>>" + http.readyState);
if(http.readyState == 4){
console.log('SUCCESS :',i);
resolve();
}
}
http.send(params);
});
}
(async () => {
for (let i = 1; i < 5; i++) {
await XHRpost(i);
}
})();
JavaScriptコードは単一のスレッドで実行されるため、ページの操作性に深刻な影響を与えずに、最初のループの反復が完了するのを待って次のループを開始することを原則的にブロックすることはできません。
ソリューションは、本当に必要なものに依存します。例がまさに必要なものに近い場合、i
を非同期プロセスに渡すという@Simonの提案は良いものです。