在javascript内存中递归构建承诺链的注意事项

在javascript内存中递归构建承诺链的注意事项,javascript,recursion,promise,Javascript,Recursion,Promise,在中,承诺链是递归构建的 稍微简化一下,我们有: function foo() { function doo() { // always return a promise if (/* more to do */) { return doSomethingAsync().then(doo); } else { return Promise.resolve(); } }

在中,承诺链是递归构建的

稍微简化一下,我们有:

function foo() {
    function doo() {
        // always return a promise
        if (/* more to do */) {
            return doSomethingAsync().then(doo);
        } else {
            return Promise.resolve();
        }
    }
    return doo(); // returns a promise
}
这可能会产生一个调用堆栈和一个承诺链,即“深”和“宽”

我预计内存峰值会比执行递归或单独构建承诺链更大

  • 是这样吗
  • 有没有人考虑过以这种方式构建链的内存问题
  • promise LIB之间的内存消耗会有所不同吗

免责声明:过早优化是不好的,找出性能差异的真正方法是对代码进行基准测试,您不必担心这一点(我只做过一次,而且我已经为至少100个项目使用了承诺)

是这样吗

是的,承诺必须“记住”它们所遵循的内容,如果你为10000个承诺这样做,你就会有一个10000长的承诺链,如果你不这样做,那么你就不会(例如,使用递归)-这对于任何排队流控制都是正确的

如果你必须记录10000件额外的事情(操作),那么你需要为它保留内存,这需要时间,如果这个数字是一百万,那么可能不可行。这因图书馆而异

有没有人考虑过以这种方式构建链的内存问题

当然,这是一个大问题,也是使用类似于
Promise.each
的东西的一个用例,在像bluebird over
这样的库中,每个
都可以链接

我个人在我的代码中有一个避免这种风格的快速应用程序,它一次遍历VM中的所有文件-但在绝大多数情况下,这不是问题

promise LIB之间的内存消耗会有所不同吗

是的,非常好。例如,如果bluebird 3.0检测到承诺操作已经是异步的(例如,如果它以promise.delay开始),它将不分配额外的队列,而只是同步执行(因为异步保证已经被保留)

这意味着我在回答第一个问题时所说的并不总是正确的(但在常规用例中是正确的):“本机承诺”永远无法做到这一点,除非提供内部支持

再说一次——这并不奇怪,因为promise库之间存在数量级的差异

调用堆栈和承诺链-即“深”和“宽”

事实上,没有。我们从
doSomeThingAsynchronous.then(doSomeThingAsynchronous.then(doSomeThingAsynchronous.then)(doSomeThingAsynchronous)…
(这就是
promise.each
promise.reduce
中所知道的承诺链,如果以这种方式编写,它可能会顺序执行处理程序)

我们在这里面临的是一个解析链1-当递归的基本情况得到满足时,最终会发生类似于
Promise.resolve(Promise.resolve(Promise.resolve(…))
。如果你想这样称呼的话,这只是“深”,而不是“宽”

我预计内存峰值会比执行递归或单独构建承诺链更大

实际上一个钉子也没有。随着时间的推移,你会慢慢地建立一大堆承诺,这些承诺都是用最里面的承诺来解决的,所有这些承诺都代表着相同的结果。当任务结束时,条件得到满足,并且最里面的承诺用实际值解决时,所有这些承诺都应该用相同的值解决。这将导致执行解析链的成本
O(n)
。之后,除了最外层的承诺,所有承诺都可以被垃圾收集

相比之下,由以下内容构建的承诺链

[…].reduce(function(prev, val) {
    // successive execution of fn for all vals in array
    return prev.then(() => fn(val));
}, Promise.resolve())
将显示一个峰值,同时分配
n
promise对象,然后缓慢地逐个解析它们,对以前的对象进行垃圾收集,直到只有已解决的end promise处于活动状态

memory
  ^     resolve      promise "then"    (tail)
  |      chain          chain         recursion
  |        /|           |\
  |       / |           | \
  |      /  |           |  \
  |  ___/   |___     ___|   \___     ___________
  |
  +----------------------------------------------> time
是这样吗

不一定。如上所述,所有的大量承诺最终都是用相同的值2解决的,因此我们所需要的就是同时存储最外层和最内层的承诺。所有中间承诺可能会尽快被垃圾收集,我们希望在恒定的空间和时间内运行此递归

事实上,对于动态条件(没有固定数量的步骤),这种递归构造是完全必要的,您无法真正避免它。在Haskell中,
IO
monad一直使用这种方法,正是因为这种情况,才对它进行了优化。它非常类似于,编译器通常会消除它

有没有人考虑过以这种方式构建链的内存问题

对。例如,这是一个例子,尽管还没有结果

许多promise库确实支持迭代帮助器,以避免promise
then
链的尖峰,如Bluebird的
each
map
方法

我自己的promise Library 3,4在不引入内存或运行时开销的情况下提供解析链。当一个承诺采纳了另一个承诺(即使仍然悬而未决),它们就变得难以区分,中间承诺也不再在任何地方引用

promise LIB之间的内存消耗会有所不同吗

对。虽然这种情况可以优化,但很少优化。具体来说,ES6规范确实要求承诺在每次
resolve
调用时检查值,因此不可能折叠链。链中的承诺甚至可以用不同的值来解析(通过构造滥用getter的示例对象,而不是在现实生活中)。但这个问题仍未解决

因此,如果您使用泄漏实现,但需要异步递归,那么最好切换回回调,并使用
function foo() {
    function doo() {
        // always return a promise
        if (/* more to do */) {
            return doSomethingAsync().then(function(){
                        throw "next";
                    }).catch(function(err) {
                        if (err == "next") doo();
                    })
        } else {
            return Promise.resolve();
        }
    }
    return doo(); // returns a promise
}
const powerp = (base, exp) => exp === 0 
 ? Promise.resolve(1)
 : new Promise(res => setTimeout(res, 0, exp)).then(
   exp => power(base, exp - 1).then(x => x * base)
 );

powerp(2, 8); // Promise {...[[PromiseValue]]: 256}
// apply powerp with 2 and 8 and substitute the recursive case:

8 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 8)).then(
  res => 7 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 7)).then(
    res => 6 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 6)).then(
      res => 5 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 5)).then(
        res => 4 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 4)).then(
          res => 3 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 3)).then(
            res => 2 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 2)).then(
              res => 1 === 0 ? Promise.resolve(1) : new Promise(res => setTimeout(res, 0, 1)).then(
                res => Promise.resolve(1)
              ).then(x => x * 2)
            ).then(x => x * 2)
          ).then(x => x * 2)
        ).then(x => x * 2)
      ).then(x => x * 2)
    ).then(x => x * 2)
  ).then(x => x * 2)
).then(x => x * 2); // Promise {...[[PromiseValue]]: 256}
var funcA = function() { 
    setTimeout(function() {console.log("funcA")}, 2000);
};
var funcB = function() { 
    setTimeout(function() {console.log("funcB")}, 1000);
};
sequence().chain(funcA).chain(funcB).execute();