Асинхронный цикл jQuery Deferreds (обещания)

Я пытаюсь создать то, что, я думаю, называется "Водопад". Я хочу последовательно обрабатывать массив асинхронных функций (обещания jQuery).

Вот надуманный пример:

function doTask(taskNum){
    var dfd = $.Deferred(), 
        time = Math.floor(Math.random()*3000);

    setTimeout(function(){
        console.log(taskNum);
        dfd.resolve();
    },time)

    return dfd.promise();
}

var tasks = [1,2,3];

for (var i = 0; i < tasks.length; i++){
    doTask(tasks[i]);
}

console.log("all done");

Я хотел бы, чтобы он выполнил задачу в порядке их выполнения (присутствует в массиве). Итак, в этом примере я хочу, чтобы он выполнил задачу 1 и дождался ее решения, затем выполнил задачу 2, дождался ее решения, выполнил задачу 3 и т. Д. И журнал "все выполнено".

Может быть, это действительно очевидно, но я пытался выяснить это весь день.

5 ответов

Решение

Я бы попробовал использовать $().queue вместо $.Deferred Вот. Добавьте функции в очередь и вызывайте следующую, только когда будете готовы.

function doTask(taskNum, next){
    var time = Math.floor(Math.random()*3000);

    setTimeout(function(){
        console.log(taskNum);
        next();
    },time)
}

function createTask(taskNum){
    return function(next){
        doTask(taskNum, next);
    }
}

var tasks = [1,2,3];

for (var i = 0; i < tasks.length; i++){
    $(document).queue('tasks', createTask(tasks[i]));
}

$(document).queue('tasks', function(){
    console.log("all done");
});

$(document).dequeue('tasks');

Для водопада вам нужен асинхронный цикл:

(function step(i, callback) {
    if (i < tasks.length)
        doTask(tasks[i]).then(function(res) {
            // since sequential, you'd usually use "res" here somehow
            step(i+1, callback);
        });
    else
        callback();
})(0, function(){
    console.log("all done");
});

Вы можете создать разрешенный $.Deferred и просто добавлять в цепочку с каждой итерацией:

var dfd = $.Deferred().resolve();
tasks.forEach(function(task){
    dfd = dfd.then(function(){
        return doTask(task);
    });
});

Шаг за шагом происходит следующее:

//begin the chain by resolving a new $.Deferred
var dfd = $.Deferred().resolve();

// use a forEach to create a closure freezing task
tasks.forEach(function(task){

    // add to the $.Deferred chain with $.then() and re-assign
    dfd = dfd.then(function(){

        // perform async operation and return its promise
        return doTask(task);
    });

});

Лично я нахожу это чище, чем рекурсия и более знакомым, чем $(). Queue (jQuery API для $(). Queue сбивает с толку, так как он предназначен для анимации, также вероятно, что вы используете $.Deferred в других местах вашего код). Он также обладает преимуществами стандартной передачи результатов по водопаду с помощью resol () в асинхронной операции и позволяет присоединять свойство $.done.

Вот это в jsFiddle

Взгляните на $.when, а затем на методы для запуска deferreds.

Водопады используются для последовательной передачи возвращаемых значений от одного отсроченного к другому. Это будет выглядеть примерно так.

function doTask (taskNum) {
  var dfd = $.Deferred(),
      time = Math.floor(Math.random() * 3000);

  console.log("running task " + taskNum);

  setTimeout(function(){
      console.log(taskNum + " completed");
      dfd.resolve(taskNum + 1);
  }, time)

  return dfd.promise();
}

var tasks = [1, 2, 3];

tasks
  .slice(1)
  .reduce(function(chain) { return chain.then(doTask); }, doTask(tasks[0]))
  .then(function() { console.log("all done"); });

Обратите внимание на аргумент, переданный resolve, Это передается следующей функции в цепочке. Если вы просто хотите запустить их последовательно, без аргументов, вы можете убрать это и изменить вызов .reduce(function(chain, taskNum) { return chain.then(doTask.bind(null, taskNum)); }, doTask(tasks[0]));

И параллельно это будет выглядеть так:

var tasks = [1,2,3].map(function(task) { return doTask(task); });

$.when.apply(null, tasks).then(function() { 
    console.log(arguments); // Will equal the values passed to resolve, in order of execution.
});

Аргументы

  • items: массив аргументов
  • func: асинхронная функция
  • обратный вызов: функция обратного вызова
  • обновление: функция обновления

Простая петля:

var syncLoop = function(items, func, callback) {
    items.reduce(function(promise, item) {
        return promise.then(func.bind(this, item));
    }, $.Deferred().resolve()).then(callback);
};

syncLoop(items, func, callback);

Отслеживание прогресса:

var syncProgress = function(items, func, callback, update) {
    var progress = 0;
    items.reduce(function(promise, item) {
        return promise.done(function() {
            update(++progress / items.length);
            return func(item);
        });
    }, $.Deferred().resolve()).then(callback);
};

syncProgress(items, func, callback, update);

Действительно интересный вызов. Я придумал рекурсивную функцию, которая принимает список и необязательный начальный индекс.

Вот ссылка на jsFiddle, который я протестировал с несколькими разными длинами списка и интервалами.

Я предполагаю, что у вас есть список функций, которые возвращают обещания (не список чисел). Если у вас есть список номеров, вы бы изменили эту часть

$.when(tasks[index]()).then(function(){
    deferredSequentialDo(tasks, index + 1);
});

к этому

/* Proxy is a method that accepts the value from the list
   and returns a function that utilizes said value
   and returns a promise  */
var deferredFunction = myFunctionProxy(tasks[index]);

$.when(tasks[index]()).then(function(){
    deferredSequentialDo(tasks, index + 1);
});

Я не уверен, насколько большим может быть ваш список функций, но просто имейте в виду, что браузер будет удерживать ресурсы с первого вызова deferredSequentialDo, пока они не будут завершены.

Другие вопросы по тегам