Шаблон проектирования для управления несколькими асинхронными операциями JavaScript
Я ищу хороший шаблон для отслеживания множества различных асинхронных действий JavaScript (загрузка изображений, несколько вызовов AJAX, последовательные вызовы AJAX и т.д.), которые лучше, чем просто множество пользовательских обратных вызовов и настраиваемого состояния переменные. Что бы вы посоветовали мне использовать? Существует ли какая-либо система очередей с возможностью иметь логику за пределами последовательности?
Пример задачи
У меня есть последовательность запуска, которая включает в себя несколько асинхронных процессов (загрузка изображений, ожидание таймеров, создание некоторых аякс-вызовов, выполнение некоторой инициализации). Некоторые из асинхронных процессов могут запускаться одновременно (загрузка изображений, вызовы AJAX), а некоторые из них должны быть упорядочены (запустить AJAX-вызов №1, затем вызов AJAX №2). Прямо сейчас у меня есть все, что работает от функций обратного вызова и кучи глобального состояния, которое отслеживает то, что было или не было выполнено. Он работает, но он довольно грязный, и у меня было несколько ошибок из-за сложностей, связанных с тем, что вы обрабатываете все возможности последовательности и правильные условия.
Когда у вас есть проблемы, это также довольно больно отлаживать, потому что это похоже на принцип неопределенности Гейзенберга. Как только вы установите точку останова в любом месте последовательности, все изменится. Вы должны поместить в код всевозможные отладочные операторы, чтобы попытаться понять, что происходит.
Вот более конкретное описание:
-
Загружается три изображения. Как только загружается одно конкретное изображение, я хочу его отобразить. Когда он будет отображаться в течение определенного промежутка времени, я хочу отобразить второе изображение. Третий отправляется в очередь для последующего отображения.
-
Существует три вызова AJAX, которые должны выполняться в последовательном порядке (выход одного из них используется как часть ввода следующего).
-
Когда выполняются вызовы AJAX, есть куча JS-обработки результатов, а затем нужно загрузить еще два изображения.
-
Когда загружаются эти два изображения, есть еще несколько материалов для отображения.
-
Когда все это будет сделано, вы изучите, как долго отображалось одно из изображений, и если прошло достаточно времени, отобразите следующее изображение. Если нет, подождите еще некоторое время перед отображением следующего изображения.
Каждый шаг имеет как успешные, так и обработчики ошибок. Некоторые из обработчиков ошибок запускают альтернативный код, который может продолжать успешно.
Я не ожидаю, что кто-нибудь последует за точным процессом здесь, но просто для того, чтобы дать людям представление о типе логики между этапами.
Изменить: я наткнулся на YUI AsyncQueue, который не является полным решением для типа проблемы, которую я имею, но находится в том же пространстве. Кажется, что это больше для упорядочивания или упорядочения множества асинхронных операций, но я не вижу, как это помогает с типом принятия решения, которое у меня есть.
Ответы
Ответ 1
С promises теперь стандартным в ES6 и множеством хороших библиотек обещаний, расширяющих его как для новых функций, так и для обратной совместимости, кажется, что promises - путь сюда.
Решение начинается с выполнения каждой операции async и создания оболочки, которая возвращает обещание:
Для загрузки изображения:
function loadImage(url) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(url);
};
img.src = url;
});
}
Для вызова Ajax (упрощенная версия):
function ajaxGet(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.addEventListener("load", resolve);
req.addEventListener("error", reject);
req.addEventListener("abort", reject);
req.open("GET", url);
req.send();
});
}
Для ожидания определенного количества времени перед выполнением операции вы даже можете сделать обедневную версию setTimeout()
, чтобы ее можно было прикован к другим операциям с обещаниями:
// delay, return a promise
// val is optional
function delay(t, val) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(val);
}, t);
});
}
Теперь их можно объединить, чтобы создать логику заданного вопроса:
Загружается три изображения. Как только загружается одно конкретное изображение, я хочу его отобразить. Когда он будет отображаться в течение определенного промежутка времени, я хочу отобразить второе изображение. Третий отправляется в очередь для последующего отображения.
// start all three images loading in parallel, get a promise for each one
var imagePromises = [url1, url2, url3].map(function(item) {
return loadImage(item);
});
// display the three images in sequence with a delay between them
imagePromises.reduce(function(p, item) {
return p.then(function() {
// when the next image is ready display it
return item.then(function(img) {
displayImage(img);
return delay(15 * 1000);
});
});
}, Promise.resolve());
Это использование .reduce()
показывает классический шаблон проектирования для последовательности серии операций над массивом с использованием promises.
Существует три вызова AJAX, которые должны выполняться в последовательном порядке (выход одного из них используется как часть ввода следующего).
и
Когда выполняются вызовы AJAX, есть куча JS-обработки результатов, а затем нужно загрузить еще два изображения.
var p = ajaxGet(url1).then(function(results1) {
// do something with results1
return ajaxGet(url2);
}).then(function(results2) {
// do something with results2
return ajaxGet(url3);
}).then(function(results3) {
// process final results3 here
// now load two images and when they are loaded do some display stuff
return Promise.all(loadImage(imgx), loadImage(imgy)).then(function(imgs) {
doSomeDisplayStuff(imgs);
});
});
Ответ 2
Взгляните на концепцию Promises/A. jQuery реализует это с помощью объекта jQuery.Deferred
.
Вот хорошая статья показывающая, как она может быть полезна для вашей ситуации. Я попросил повторить аналогичную question.
Ответ 3
Хотя Promises дает нам более чистый код, мы можем сделать лучше с генераторами. Я написал сообщение о том, как использовать генераторы в Tame Async JavaScript с ES6. Использование описанного шаблона облегчит управление сложными асинхронными взаимодействиями и создаст ментальную модель для возможностей await
, запланированных для ES7.