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