在javascript中递归建立一个promise链 - 内存考虑因素

Roa*_*888 55 javascript recursion promise

这个答案中,一个承诺链是递归建立的.

稍微简化,我们有:

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
}
Run Code Online (Sandbox Code Playgroud)

据推测,这会产生一个调用堆栈一个承诺链 - 即"深"和"宽".

我预计内存峰值会大于执行递归或单独建立一个promise链.

  • 是这样吗?
  • 有没有人考虑过以这种方式建立连锁店的记忆问题?
  • 承诺库之间的内存消耗会有所不同吗?

Ber*_*rgi 44

调用堆栈和承诺链 - 即"深"和"宽".

实际上,没有.这里没有任何承诺链,因为我们知道它doSomeThingAsynchronous.then(doSomethingAsynchronous).then(doSomethingAsynchronous).…(如果以这种方式编写,那么顺序执行处理程序是什么Promise.each或者Promise.reduce可能做什么).

我们在这里面临的是一个解决方案链1 - 最终,当满足递归的基本情况时,会发生什么Promise.resolve(Promise.resolve(Promise.resolve(…))).如果你想称它为"深",而不是"宽".

我预计内存峰值会大于执行递归或单独建立一个promise链.

实际上并不是一个高峰.随着时间的推移,你会慢慢地构建大量的承诺,这些承诺用最里面的一个解决,所有的承诺都代表相同的结果.在任务结束时,条件得到满足并且最内层的承诺用实际值解决时,所有这些承诺都应该用相同的值来解决.这最终会O(n)带来走向解析链的成本(如果天真地实现,这甚至可以递归地完成并导致堆栈溢出).在那之后,除了最外面的所有承诺都可以变成垃圾收集.

相反,一个由类似的东西建立的承诺链

[…].reduce(function(prev, val) {
    // successive execution of fn for all vals in array
    return prev.then(() => fn(val));
}, Promise.resolve())
Run Code Online (Sandbox Code Playgroud)

会显示一个尖峰,同时分配npromise对象,然后逐个慢慢地解决它们,垃圾收集前一个,直到只有已确定的最终承诺存活.

memory
  ^     resolve      promise "then"    (tail)
  |      chain          chain         recursion
  |        /|           |\
  |       / |           | \
  |      /  |           |  \
  |  ___/   |___     ___|   \___     ___________
  |
  +----------------------------------------------> time
Run Code Online (Sandbox Code Playgroud)

是这样吗?

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

事实上,这种递归构造对于具有动态条件的异步循环(没有固定数量的步骤)是完全必要的,你无法真正避免它.在Haskell中,IOmonad 一直使用它,因为这种情况实现了对它的优化.它与尾调用递归非常相似,它通常由编译器消除.

有没有人考虑过以这种方式建立连锁店的记忆问题?

是.这是在承诺/ Aplus的讨论,例如,虽然没有结局呢.

许多promise库确实支持迭代助手,以避免then像Bluebird eachmap方法那样的promise 链的飙升.

我自己的promise库3,4确实具有解析链,而不会引入内存或运行时开销.当一个承诺采用另一个承诺(即使仍然悬而未决)时,它们变得难以区分,并且中间承诺不再在任何地方引用.

承诺库之间的内存消耗会有所不同吗?

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

因此,如果您使用泄漏实现,但需要异步递归,那么您最好切换回回调并使用延迟反模式将最内层的promise结果传播到单个结果承诺.

[1]:没有官方术语
[2]:嗯,他们是相互解决的.但是,我们希望用相同的值来解决这些问题,我们期待的是
[3]:无证操场,通过Aplus的.阅读代码是您自己的危险:https://github.com/bergus/F-Promise
[4]:也在此拉取请求中为Creed实现

  • 接受了答复,谢谢.有人读这篇文章,请阅读Benjamin Gruenbaum的回答,这进一步增加了价值. (2认同)

Ben*_*aum 15

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

是这样吗?

是的,承诺必须"记住"他们所关注的内容,如果你为10000承诺执行此操作,你将拥有10000长的承诺链,如果你没有,那么你就不会(例如,递归) - 对于任何排队流量控制都是如此.

如果你必须跟踪10000个额外的东西(操作),那么你需要为它保留内存,这需要时间,如果这个数字是一百万,它可能是不可行的.这在图书馆中有所不同

有没有人考虑过以这种方式建立连锁店的记忆问题?

当然,这是一个很大的问题,也是一个用例,比如Promise.each像bluebird 这样的库,then可以用来链接.

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

承诺库之间的内存消耗会有所不同吗?

是的,非常好.例如,如果bluebird 3.0检测到promise操作已经异步(例如,如果它以Promise.delay开头)并且只是同步执行事务(因为异步保证已经保留),蓝鸟3.0将不会分配额外的队列.

这意味着我在第一个问题的答案中声称的并不总是正确的(但在常规用例中也是如此):)除非提供内部支持,否则本机承诺将永远无法执行此操作.

然后再次 - 这并不奇怪,因为承诺库彼此之间存在数量级的差异.

  • 哇,那很快.我的意思是,[我已被警告](http://stackoverflow.com/questions/29904445/draw-continuous-lines-on-html-canvas-one-after-the-other/29906627?noredirect=1#comment47973639_29906627 ),但无法与此竞争...... (2认同)

dot*_*hlu 5

我只是提出了一个可以帮助解决问题的技巧:不要在最后进行递归then,而应该在最后进行递归catch,因为catch它不在解决链范围内。以您的示例为例:

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
}
Run Code Online (Sandbox Code Playgroud)

  • 现在我们通过讨论得出了结论,为什么不进一步解决这个问题呢?我自己遇到了问题,因此我通过搜索到达了此页面,也许还有其他读者想知道如何解决该问题,为什么不让他们在这里找到答案?既然这是你的话题?这是社区,下次询问时请检查条款,内容属于谁? (8认同)