jQuery Deferreds(承诺)的异步循环

22

我正在尝试创建一个被称为“瀑布流”的东西。我想顺序处理一系列异步函数(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等等,最后记录"all done"。

也许这很显然,但是我整个下午都在尝试弄清楚这个问题。

6个回答

22

我建议在这里尝试使用$().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');

1
这真是独一无二的!为我解决了一个非常困难的问题。非常感谢你。 - Veysel Ozdemir

9
对于一个瀑布流,你需要一个异步循环:
(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");
});

8
你可以创建一个已解决的$.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 设计用于动画,很容易让人感到困惑,而且你可能在代码的其他地方使用了 $.Deferred)。它还具有通过异步操作中的 resolve() 标准传输结果并允许附加 $.done 属性的好处。

这是一个 jsFiddle


5

请查看$.whenthen方法来运行延迟对象。

瀑布流用于按顺序将返回值从一个延迟对象传递到下一个延迟对象。它看起来会像这样

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调用更改为.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.
});

使用reduce和map的方式非常优雅。 - thedarklord47

0

确实是一个有趣的挑战。我想到了一个递归函数,它接受一个列表和一个可选的起始索引。

这里是我在 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调用的资源直到它们全部完成。


"deferredSync" 听起来像是个矛盾词。 - Bergi
然而,如果您有几个想要同步执行的ajax调用(这是我在实践中遇到过的情况),那么我可以看到这个功能的用途。 - awbergs
Ajax不是同步的。你是什么意思? - Bergi
我知道这不是同步的,但如果 ajax 调用 #2 依赖于 ajax 调用 #1 的结果,则它们需要一个接一个地执行,而不是同时执行。 - awbergs
但这是“顺序”的,而“延迟”才是正确的术语。没有任何sync - Bergi
1
是的,我的错,我把连续和同步混淆了。我更新了我的答案和jsFiddle。 - awbergs

0

参数

  • items: 参数数组
  • func: 异步函数
  • callback: 回调函数
  • update: 更新函数

简单循环:

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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接