Асинхронный процесс внутри цикла JavaScript для

Я запускаю цикл обработки событий следующей формы:

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

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

Я пытаюсь отобразить серию предупреждений с номерами от 0 до 10. Проблема заключается в том, что к моменту запуска функции обратного вызова цикл уже прошел несколько итераций и отображает более высокое значение i, Любые рекомендации о том, как это исправить?

6 ответов

Решение

for цикл запускается сразу до завершения, пока все ваши асинхронные операции запущены. Когда они завершат какое-то время в будущем и вызовут свои обратные вызовы, значение вашей индексной переменной цикла i будет в последнем значении для всех обратных вызовов.

Это потому что for Цикл не ожидает завершения асинхронной операции, прежде чем перейти к следующей итерации цикла, и потому что асинхронные обратные вызовы будут вызваны в будущем. Таким образом, цикл завершает свои итерации, и затем обратные вызовы вызываются, когда эти асинхронные операции завершаются. Таким образом, индекс цикла "готов" и находится в своем окончательном значении для всех обратных вызовов.

Чтобы обойти это, вы должны уникальным образом сохранить индекс цикла отдельно для каждого обратного вызова. В Javascript способ сделать это - захватить его в закрытии функции. Это может быть сделано либо созданием встроенной функции закрытия специально для этой цели (первый пример показан ниже), либо вы можете создать внешнюю функцию, которой вы передадите индекс, и позволить ему поддерживать индекс уникально для вас (второй пример показан ниже).

Начиная с 2016 года, если у вас есть полная реализация Javascript для ES6, вы также можете использовать let определить for переменная цикла, и она будет однозначно определена для каждой итерации for цикл (третья реализация ниже). Но обратите внимание, что это поздняя реализация в реализациях 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);
    });
}

Используйте ES6let

Если у вас есть среда выполнения Javascript, полностью поддерживающая ES6, вы можете использовать let в вашем for цикл как это:

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

let объявлен в for Объявление цикла, как это создаст уникальное значение i для каждого вызова цикла (что вы хотите).

Сериализация с обещаниями и асинхронностью / ожиданием

Если ваша асинхронная функция возвращает обещание, и вы хотите сериализовать асинхронные операции, которые будут выполняться одна за другой, а не параллельно, и вы работаете в современной среде, которая поддерживает 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() в полете одновременно, и for цикл не будет продвигаться до тех пор, пока все не будет сделано. Это отличается от предыдущих схем, в которых все асинхронные операции выполнялись параллельно, поэтому полностью зависит от того, какой дизайн вы хотите. Замечания: await работает с обещанием, поэтому ваша функция должна возвращать обещание, которое разрешается / отклоняется после завершения асинхронной операции. Также обратите внимание, что для того, чтобы использовать 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();

понимание Promises прежде чем даже смотреть в async await это необходимо (также читайте о поддержке 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 или пользовательская функция карри (которая не нуждается в объекте контекста).

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), возвращая обещание (асинхронный код внутри обещания).

Затем вызовите функцию (XHRPost) внутри цикла for, но с волшебным ключевым словом Await.:)

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 чтобы ваш асинхронный процесс был хорошим.

Другие вопросы по тегам