Ответ 1
Вот несколько простых примеров того, как вы последовательно проходите через массив, выполняя каждую асинхронную операцию последовательно (один за другим).
Предположим, у вас есть массив элементов:
var arr = [...];
И вы хотите выполнить определенную асинхронную операцию для каждого элемента в массиве, поочередно, так, чтобы следующая операция не начиналась до тех пор, пока не завершится предыдущая.
И, допустим, у вас есть функция возврата обещания для обработки одного из элементов в массиве fn(item)
:
Ручная итерация
function processItem(item) {
// do async operation and process the result
// return a promise
}
Затем вы можете сделать что-то вроде этого:
function processArray(array, fn) {
var index = 0;
function next() {
if (index < array.length) {
fn(array[index++]).then(next);
}
}
next();
}
processArray(arr, processItem);
Ручная итерация, возвращающая обещание
Если вы хотите вернуть обещание из processArray()
чтобы знать, когда оно выполнено, вы можете добавить к нему следующее:
function processArray(array, fn) {
var index = 0;
function next() {
if (index < array.length) {
return fn(array[index++]).then(function(value) {
// apply some logic to value
// you have three options here:
// 1) Call next() to continue processing the result of the array
// 2) throw err to stop processing and result in a rejected promise being returned
// 3) return value to stop processing and result in a resolved promise being returned
return next();
});
}
} else {
// return whatever you want to return when all processing is done
// this returne value will be the ersolved value of the returned promise.
return "all done";
}
}
processArray(arr, processItem).then(function(result) {
// all done here
console.log(result);
}, function(err) {
// rejection happened
console.log(err);
});
Примечание: это остановит цепочку при первом отклонении и передаст эту причину обратно возвращенному обещанию processArray.
Итерация с .reduce()
Если вы хотите выполнить больше работы с обещаниями, вы можете связать все обещания:
function processArray(array, fn) {
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item);
});
}, Promise.resolve());
}
processArray(arr, processItem).then(function(result) {
// all done here
}, function(reason) {
// rejection happened
});
Примечание: это остановит цепочку при первом отклонении и передаст эту причину обратно обещанию, возвращенному из processArray()
.
В случае успеха обещание, возвращаемое из processArray()
будет разрешено с последним разрешенным значением вашего обратного вызова fn
. Если вы хотите накапливать список результатов и обрабатывать их, вы можете собрать результаты в массиве замыканий из fn
и продолжать возвращать этот массив каждый раз, чтобы окончательное решение было массивом результатов.
Итерация с .reduce(), которая разрешается с массивом
И, поскольку теперь кажется очевидным, что вы хотите, чтобы конечный результат обещания представлял собой массив данных (по порядку), вот ревизия предыдущего решения, которое производит это:
function processArray(array, fn) {
var results = [];
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item).then(function(data) {
results.push(data);
return results;
});
});
}, Promise.resolve());
}
processArray(arr, processItem).then(function(result) {
// all done here
// array of data here in result
}, function(reason) {
// rejection happened
});
Рабочая демонстрация: http://jsfiddle.net/jfriend00/h3zaw8u8/
И рабочая демонстрация, которая показывает отказ: http://jsfiddle.net/jfriend00/p0ffbpoc/
Итерация с .reduce(), которая разрешается с массивом с задержкой
И, если вы хотите вставить небольшую задержку между операциями:
function delay(t, v) {
return new Promise(function(resolve) {
setTimeout(resolve.bind(null, v), t);
});
}
function processArrayWithDelay(array, t, fn) {
var results = [];
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item).then(function(data) {
results.push(data);
return delay(t, results);
});
});
}, Promise.resolve());
}
processArray(arr, 200, processItem).then(function(result) {
// all done here
// array of data here in result
}, function(reason) {
// rejection happened
});
Итерация с библиотекой обещаний Bluebird
Библиотека обещаний Bluebird имеет много встроенных функций управления параллелизмом. Например, для последовательной итерации по массиву вы можете использовать Promise.mapSeries()
.
Promise.mapSeries(arr, function(item) {
// process each individual item here, return a promise
return processItem(item);
}).then(function(results) {
// process final results here
}).catch(function(err) {
// process array here
});
Или вставить задержку между итерациями:
Promise.mapSeries(arr, function(item) {
// process each individual item here, return a promise
return processItem(item).delay(100);
}).then(function(results) {
// process final results here
}).catch(function(err) {
// process array here
});
Использование ES7 async/await
Если вы кодируете в среде, которая поддерживает async/await, вы также можете просто использовать регулярный цикл for
а затем await
обещание в цикле, и это заставит цикл for
приостановиться, пока обещание не будет разрешено, прежде чем продолжить. Это эффективно упорядочит ваши асинхронные операции, поэтому следующая не начнется, пока не будет выполнена предыдущая.
async function processArray(array, fn) {
let results = [];
for (let i = 0; i < array.length; i++) {
let r = await fn(array[i]);
results.push(r);
}
return results; // will be resolved value of promise
}
// sample usage
processArray(arr, processItem).then(function(result) {
// all done here
// array of data here in result
}, function(reason) {
// rejection happened
});
К вашему сведению, моя processArray()
здесь очень похожа на Promise.map()
в библиотеке обещаний Bluebird, которая принимает массив и функцию создания обещаний и возвращает обещание, которое разрешается с массивом разрешенных результатов.
@vitaly-t - Вот несколько более подробных комментариев о вашем подходе. Добро пожаловать в любой код, который вам кажется лучшим. Когда я впервые начал использовать обещания, я имел тенденцию использовать обещания только для самых простых вещей, которые они делали, и сам писал большую логику, когда более продвинутое использование обещаний могло бы сделать для меня гораздо больше. Вы используете только то, что вам удобно, и даже больше, вы предпочитаете видеть свой собственный код, который вы глубоко знаете. Это, вероятно, человеческая природа.
Я предположу, что, поскольку я все больше и больше понимал, что обещания могут сделать для меня, мне теперь нравится писать код, который использует больше продвинутых функций обещаний, и он кажется мне совершенно естественным, и я чувствую, что хорошо строю проверенная инфраструктура, которая имеет много полезных функций. Я бы только попросил вас держать свой разум открытым, поскольку вы все больше и больше учитесь, чтобы потенциально идти в этом направлении. По моему мнению, это полезное и продуктивное направление для миграции по мере улучшения вашего понимания.
Вот некоторые конкретные отзывы о вашем подходе:
Вы создаете обещания в семи местах
В отличие от стилей, в моем коде есть только два места, где я явно создаю новое обещание - один раз в фабричной функции и один раз для инициализации цикла .reduce()
. В другом месте я просто опираюсь на обещания, которые уже были созданы цепочкой или возвращением значений внутри них, или просто возвращением их напрямую. В вашем коде есть семь уникальных мест, где вы создаете обещание. Теперь хорошее кодирование - это не соревнование, чтобы увидеть, как мало мест, где вы можете создать обещание, но это может указать на разницу в использовании обещаний, которые уже созданы, в сравнении с условиями тестирования и созданием новых обещаний.
Бросок безопасности очень полезная функция
Обещания безопасны Это означает, что исключение, сгенерированное в обработчике обещания, автоматически отклонит это обещание. Если вы просто хотите, чтобы исключение стало отклонением, воспользуйтесь этой полезной функцией. На самом деле, вы обнаружите, что простое бросание - это полезный способ отказаться изнутри обработчика без создания еще одного обещания.
Много Promise.resolve()
или Promise.reject()
, вероятно, возможность для упрощения
Если вы видите код с множеством Promise.resolve()
или Promise.reject()
, то, вероятно, есть возможности лучше использовать существующие обещания, а не создавать все эти новые обещания.
Приведение к обещанию
Если вы не знаете, вернул ли что-то обещание, вы можете привести его к обещанию. Затем библиотека обещаний самостоятельно проверяет, является ли это обещание или нет, и даже соответствует ли оно виду, который соответствует используемой вами библиотеке обещаний, и, если нет, объединяет его в одно. Это может спасти переписывание большей части этой логики самостоятельно.
Договор на возврат обещания
Во многих случаях в наши дни совершенно жизненно важно иметь контракт на функцию, которая может сделать что-то асинхронное для возврата обещания. Если функция просто хочет сделать что-то синхронное, она может просто вернуть разрешенное обещание. Вы, кажется, чувствуете, что это обременительно, но это определенно то, как дует ветер, и я уже пишу много кода, который требует этого, и это становится очень естественным, когда вы знакомитесь с обещаниями. Он абстрагирует, является ли операция синхронизированной или асинхронной, и вызывающая сторона не должна знать или делать что-то особенное в любом случае. Это хорошее использование обещаний.
Функция фабрики может быть написана для создания только одного обещания
Функция фабрики может быть написана так, чтобы создать только одно обещание, а затем разрешить или отклонить его. Этот стиль также делает его безопасным, поэтому любое исключение, возникающее в заводской функции, автоматически становится отклонением. Это также делает контракт всегда возвращать обещание автоматически.
Хотя я понимаю, что эта фабричная функция является функцией-заполнителем (она даже не выполняет асинхронную работу), надеюсь, вы сможете увидеть стиль, чтобы рассмотреть ее:
function factory(idx) {
// create the promise this way gives you automatic throw-safety
return new Promise(function(resolve, reject) {
switch (idx) {
case 0:
resolve("one");
break;
case 1:
resolve("two");
break;
case 2:
resolve("three");
break;
default:
resolve(null);
break;
}
});
}
Если какая-либо из этих операций была асинхронной, то они могли бы просто вернуть свои собственные обещания, которые автоматически соединялись бы с одним центральным обещанием, подобным этому:
function factory(idx) {
// create the promise this way gives you automatic throw-safety
return new Promise(function(resolve, reject) {
switch (idx) {
case 0:
resolve($.ajax(...));
case 1:
resole($.ajax(...));
case 2:
resolve("two");
break;
default:
resolve(null);
break;
}
});
}
Использовать обработчик отклонения для return promise.reject(reason)
не требуется. return promise.reject(reason)
не нужна
Когда у вас есть это тело кода:
return obj.then(function (data) {
result.push(data);
return loop(++idx, result);
}, function (reason) {
return promise.reject(reason);
});
Обработчик отклонения не добавляет никакого значения. Вместо этого вы можете просто сделать это:
return obj.then(function (data) {
result.push(data);
return loop(++idx, result);
});
Вы уже возвращаете результат obj.then()
. Если либо obj
отклоняет, либо если что-либо, связанное с obj
или возвращаемое из него, обработчик .then()
отклоняет, obj
будет отклонен. Таким образом, вам не нужно создавать новое обещание с отклонением. Более простой код без обработчика отклонения делает то же самое с меньшим количеством кода.
Вот версия в общей архитектуре вашего кода, которая пытается включить большинство из этих идей:
function factory(idx) {
// create the promise this way gives you automatic throw-safety
return new Promise(function(resolve, reject) {
switch (idx) {
case 0:
resolve("zero");
break;
case 1:
resolve("one");
break;
case 2:
resolve("two");
break;
default:
// stop further processing
resolve(null);
break;
}
});
}
// Sequentially resolves dynamic promises returned by a factory;
function sequence(factory) {
function loop(idx, result) {
return Promise.resolve(factory(idx)).then(function(val) {
// if resolved value is not null, then store result and keep going
if (val !== null) {
result.push(val);
// return promise from next call to loop() which will automatically chain
return loop(++idx, result);
} else {
// if we got null, then we're done so return results
return result;
}
});
}
return loop(0, []);
}
sequence(factory).then(function(results) {
log("results: ", results);
}, function(reason) {
log("rejected: ", reason);
});
Рабочая демонстрация: http://jsfiddle.net/jfriend00/h3zaw8u8/
Некоторые комментарии об этой реализации:
-
Promise.resolve(factory(idx))
существу приводит результатfactory(idx)
к обещанию. Если это было просто значение, то оно становится разрешенным обещанием с этим возвращаемым значением в качестве значения разрешения. Если это уже было обещание, то оно просто цепляется за это обещание. Таким образом, он заменяет весь ваш код проверки типа на возвращаемое значение функцииfactory()
. -
Заводская функция сигнализирует, что это делается путем возврата либо
null
либо обещания, которое разрешило значение, в итоге становитсяnull
. Приведенный выше преобразователь отображает эти два условия в один и тот же результирующий код. -
Заводская функция автоматически перехватывает исключения и превращает их в отклонения, которые затем автоматически обрабатываются функцией
sequence()
. Это одно существенное преимущество, которое позволяет обещаниям выполнять большую часть обработки ошибок, если вы просто хотите прервать обработку и вернуть ошибку при первом исключении или отклонении. -
Функция фабрики в этой реализации может возвращать обещание или статическое значение (для синхронной операции), и она будет работать нормально (согласно вашему запросу на разработку).
-
Я протестировал его с брошенным исключением в обратном вызове обещания в фабричной функции, и оно действительно просто отклоняет и передает это исключение, чтобы отклонить обещание последовательности с исключением в качестве причины.
-
При этом используется тот же метод, что и вы (специально пытаясь придерживаться общей архитектуры) для объединения нескольких вызовов
loop()
.