Асинхронный процесс внутри цикла javascript for

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

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

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

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

Ответ 1

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

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

Чтобы обойти это, вы должны однозначно сохранять индекс цикла отдельно для каждого обратного вызова. В 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 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

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

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

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

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

Если функция async возвращает обещание, и вы хотите сериализовать операции async для запуска один за другим, а не параллельно, и вы работаете в современной среде, поддерживающей 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.

Ответ 2

Любые рекомендации относительно того, как это исправить?

Несколько. Вы можете использовать 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 функция (для которой не нужен объект контекста).

Ответ 3

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)

Ответ 4

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

Ответ 5

JavaScript-код работает в одном потоке, поэтому вы не можете блокировать до конца завершения первой итерации цикла до начала следующего, не оказывая серьезного влияния на удобство использования страницы.

Решение зависит от того, что вам действительно нужно. Если пример близок к тому, что вам нужно, предложение @Simon передать i вашему асинхронному процессу является хорошим.