如何使用AngularJS $q顺序链接承诺?

17
在承诺库 Q 中,您可以按以下方式按顺序链接承诺:
var items = ['one', 'two', 'three'];
var chain = Q();
items.forEach(function (el) {
  chain = chain.then(foo(el));
});
return chain;

然而,下面的内容不适用于$q

var items = ['one', 'two', 'three'];
var chain = $q();
items.forEach(function (el) {
  chain = chain.then(foo(el));
});
return chain;
7个回答

37

Redgeoff,你的回答是我曾经用来将数组转换为一系列链接的Promise的方法。

现在出现了一种流行的 事实上 模式,如下所示:

function doAsyncSeries(arr) {
    return arr.reduce(function (promise, item) {
      return promise.then(function(result) {
        return doSomethingAsync(result, item);
      });
    }, $q.when(initialValue));
}

//then
var items = ['x', 'y', 'z'];
doAsyncSeries(items).then(...);

注:

  • .reduce 是原生JavaScript,不是库的一部分。
  • result 是先前的异步结果/数据,并且为了完整性而包含在内。初始resultinitialValue。如果不需要传递`result`,则可以将其省略。
  • 根据您使用的承诺库,调整$q.when(initialValue)
  • 在您的情况下,doSomethingAsyncfoo(或foo()返回的内容?)-无论如何都是一个函数。

如果像我一样,那么这个模式乍一看起来像是难以理解的混乱代码,但是一旦你熟悉了它,你就会开始把它看作是老朋友。

编辑

这里有一个演示,旨在证明上面推荐的模式确实按顺序执行其doSomethingAsync()调用,而不是像下面的评论建议的那样立即构建链。


1
原始答案在语法上不正确,我已经进行了修正。另外,initialValue应该设置为多少呢?就像上面的答案一样,这将同时触发它们所有。 - FlavorScape
initialValuereduce() 循环的第一次迭代中显示为 result。它的值取决于应用程序。如果 doSomethingAsync() 不需要传递先前的结果,则减少初始化器将简化为 $q.when() - Roamer-1888
对于大约7000个数组项(我知道后台服务器端的工作会更好),我在进行了大约2分钟后遇到了net::ERR_INSUFFICIENT_RESOURCES错误,所以我想知道为什么会发生这种情况,即使我使用这个来限制执行为串行而不是并行。 - pulkitsinghal
1
@pulkitsinghal,你可能会喜欢阅读我的问题在JavaScript中递归构建Promise链 - 内存考虑,它实际上从稍微不同的角度开始,但吸引了两个回答,涵盖了相关内容。 - Roamer-1888
1
感谢 @Roamer-1888 提供了这个问题的简明解决方案。 - Deniz
显示剩余5条评论

34

只需使用$q.when()函数:

var items = ['one', 'two', 'three'];
var chain = $q.when();
items.forEach(function (el) {
  chain = chain.then(foo(el));
});
return chain;

注意:foo必须是一个工厂,例如:

function setTimeoutPromise(ms) {
  var defer = $q.defer();
  setTimeout(defer.resolve, ms);
  return defer.promise;
}

function foo(item, ms) {
  return function() {
    return setTimeoutPromise(ms).then(function () {
      console.log(item);
    });
  };
}

var items = ['one', 'two', 'three'];
var chain = $q.when();
items.forEach(function (el, i) {
  chain = chain.then(foo(el, (items.length - i)*1000));
});
return chain;

4
这个方法不起作用。它会同时执行所有请求。我知道这是因为我发送了一系列请求,大约需要500毫秒的时间。观察我的网络流量,它们都是同时发送的(但是有序)。 - FlavorScape
1
啊,好的,将它变成一个工厂会使得在构建链时它不会立即在调用堆栈中执行,对吧? - FlavorScape
我怎样才能知道链式中的所有 Promise 都已成功解决?chain.then$q.all(chain).then 似乎都不起作用。 - Zbynek
@Zbynek,在最后你可以使用chain.then(function () { console.log('all resolved'); }); - redgeoff
1
这里有一个类似的解决方案在codepen中。本质上使用了与@redgeoff相同的方法,但是使用了一个好用的可重复使用函数,并且增加了返回所有承诺结果的数组(就像$q.all()一样)。 - John Barton

5
var when = $q.when();

for(var i = 0; i < 10; i++){
    (function() {
         chain = when.then(function() {
        return $http.get('/data');
      });

    })(i); 
}

非常优雅!你救了我的一天 :) - user3426603

4

如果你不需要自动化的话,可以按照 redgeoff的回答所示,使用$q.when().then()链接承诺。在这篇文章的开头有演示:

return $q.when() .then(function(){ return promise1; }) .then(function(){ return promise2; });

如果您有一个不确定数量需要按顺序解决的承诺,这是否有效? - Tony Brasunas
我非常确定你可以这样做,我认为这正是redgeoff的答案所描述的。 - Matthias
是的,没错!我搞定了。可能只是我比较慢,但我认为这个页面上关于循环的解释并不像它们本应该那样清晰,所以我花了一些时间才弄明白。如果我有时间的话,可能会在这里提供一个澄清的答案。但你的回答确实有帮助。 - Tony Brasunas

4

有这个:

let items = ['one', 'two', 'three'];

一行代码(为了可读性,其实是三行):

return items
    .map(item => foo.bind(null, item))
    .reduce($q.when, $q.resolve());

2

你的答案是正确的。然而,我想提供一个替代方案。如果你经常需要串行链接承诺,那么你可能会对$q.serial感兴趣。

var items = ['one', 'two', 'three'];
var tasks = items.map(function (el) {
  return function () { foo(el, (items.length - i)*1000)); });
});

$q.serial(tasks);

function setTimeoutPromise(ms) {
  var defer = $q.defer();
  setTimeout(defer.resolve, ms);
  return defer.promise;
}

function foo(item, ms) {
  return function() {
    return setTimeoutPromise(ms).then(function () {
      console.log(item);
    });
  };
}

1
这看起来很有趣,今天我已经有一百万个单元测试要写了,写完后我会尝试重构。 - FlavorScape
这绝对是一种更简单的方法,因为以后阅读代码会更容易。 - supersan
2
$q.serial是不存在的文档页面。 - Tony_Henrich
该链接指向一些可疑的网站,试图安装某些东西。 - Claudio

2

我更喜欢使用angular.bind(或Function.prototype.bind)编写返回promise的函数,然后使用reduce快捷方式将它们链接成一个链。例如:

// getNumber resolves with given number
var get2 = getNumber.bind(null, 2);
var get3 = getNumber.bind(null, 3);
[get2, get3].reduce(function (chain, fn) {
   return chain.then(fn);
}, $q.when())
.then(function (value) {
   console.log('chain value =', value);
}).done();
// prints 3 (the last value)

在这种情况下,您能详细说明一下 angular.bind 的好处吗? - pulkitsinghal
我使用了bind来创建返回promise的函数,它们不需要任何参数。因此,它们可以像.then(foo).then(bar)一样使用。 - gleb bahmutov

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