web-dev-qa-db-ja.com

JavaScript forループ内の非同期プロセス

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

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

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

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

102
Ben Pearce

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として宣言する必要があります。

178
jfriend00

async awaitはここ(ES7)にあるので、この種のことを今非常に簡単に行うことができます。

  var i;
  var j = 10;
  for (i = 0; i < j; i++) {
    await asycronouseProcess();
    alert(i);
  }

、これはasycronouseProcessPromiseを返している場合にのみ機能することを忘れないでください

asycronouseProcessがコントロールにない場合、次のように自分でPromiseを返すようにできます。

function asyncProcess() {
  return new Promise((resolve, reject) => {
    asycronouseProcess(()=>{
      resolve();
    })
  })
}

次に、この行をawait asycronouseProcess();await asyncProcess();に置き換えます

async awaitを調べる前であってもPromisesを理解する必要がありますasync awaitのサポートについてもお読みください)

11
Praveena

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

いくつか。 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 関数(コンテキストオブジェクトを必要としません)。

10
ZER0
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();

ここに期待されるものへの機能的なアプローチの例を示します。

1
Black Mamba

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);
       }
})();
0
Sumer

JavaScriptコードは単一のスレッドで実行されるため、ページの操作性に深刻な影響を与えずに、最初のループの反復が完了するのを待って次のループを開始することを原則的にブロックすることはできません。

ソリューションは、本当に必要なものに依存します。例がまさに必要なものに近い場合、iを非同期プロセスに渡すという@Simonの提案は良いものです。

0
Eric J.