Подтвердить что ты не робот

Шаблон проектирования для управления несколькими асинхронными операциями JavaScript

Я ищу хороший шаблон для отслеживания множества различных асинхронных действий JavaScript (загрузка изображений, несколько вызовов AJAX, последовательные вызовы AJAX и т.д.), которые лучше, чем просто множество пользовательских обратных вызовов и настраиваемого состояния переменные. Что бы вы посоветовали мне использовать? Существует ли какая-либо система очередей с возможностью иметь логику за пределами последовательности?

Пример задачи

У меня есть последовательность запуска, которая включает в себя несколько асинхронных процессов (загрузка изображений, ожидание таймеров, создание некоторых аякс-вызовов, выполнение некоторой инициализации). Некоторые из асинхронных процессов могут запускаться одновременно (загрузка изображений, вызовы AJAX), а некоторые из них должны быть упорядочены (запустить AJAX-вызов №1, затем вызов AJAX №2). Прямо сейчас у меня есть все, что работает от функций обратного вызова и кучи глобального состояния, которое отслеживает то, что было или не было выполнено. Он работает, но он довольно грязный, и у меня было несколько ошибок из-за сложностей, связанных с тем, что вы обрабатываете все возможности последовательности и правильные условия.

Когда у вас есть проблемы, это также довольно больно отлаживать, потому что это похоже на принцип неопределенности Гейзенберга. Как только вы установите точку останова в любом месте последовательности, все изменится. Вы должны поместить в код всевозможные отладочные операторы, чтобы попытаться понять, что происходит.

Вот более конкретное описание:

  • Загружается три изображения. Как только загружается одно конкретное изображение, я хочу его отобразить. Когда он будет отображаться в течение определенного промежутка времени, я хочу отобразить второе изображение. Третий отправляется в очередь для последующего отображения.

  • Существует три вызова AJAX, которые должны выполняться в последовательном порядке (выход одного из них используется как часть ввода следующего).

  • Когда выполняются вызовы AJAX, есть куча JS-обработки результатов, а затем нужно загрузить еще два изображения.

  • Когда загружаются эти два изображения, есть еще несколько материалов для отображения.

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

Каждый шаг имеет как успешные, так и обработчики ошибок. Некоторые из обработчиков ошибок запускают альтернативный код, который может продолжать успешно.

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

Изменить: я наткнулся на YUI AsyncQueue, который не является полным решением для типа проблемы, которую я имею, но находится в том же пространстве. Кажется, что это больше для упорядочивания или упорядочения множества асинхронных операций, но я не вижу, как это помогает с типом принятия решения, которое у меня есть.

4b9b3361

Ответ 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);
    });
});

Ответ 3

Хотя Promises дает нам более чистый код, мы можем сделать лучше с генераторами. Я написал сообщение о том, как использовать генераторы в Tame Async JavaScript с ES6. Использование описанного шаблона облегчит управление сложными асинхронными взаимодействиями и создаст ментальную модель для возможностей await, запланированных для ES7.