在javascript中recursion地构build一个承诺链 – 内存考虑

在这个答案中 ,一个promise链是recursion地构build的。

稍微简化一下,我们有:

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 } 

据推测,这将产生一个调用堆栈承诺链 – 即“深”和“宽”。

我预计内存的峰值会比执行recursion或者单独构build一个承诺链要大。

  • 这是吗?
  • 有没有人以这种方式考虑过构build连锁店的内存问题?
  • 内存消耗会有所不同吗?

一个调用堆栈和一个承诺链 – 即“深”和“宽”。

其实没有 在doSomeThingAsynchronous.then(doSomethingAsynchronous).then(doSomethingAsynchronous).… (这是Promise.eachPromise.reduce可能会按顺序执行处理程序(如果以这种方式写入的话) doSomeThingAsynchronous.then(doSomethingAsynchronous).then(doSomethingAsynchronous).…没有承诺链。

我们在这里面临的是一个解决scheme链 1 – 当recursion的基本情况得到满足时,最终会发生什么,就像Promise.resolve(Promise.resolve(Promise.resolve(…))) 。 这只是“深”,而不是“宽”,如果你想这样称呼的话。

我预计内存的峰值会比执行recursion或者单独构build一个承诺链要大。

实际上不是一个秒杀。 随着时间的推移,你会慢慢地build立一大堆的承诺,用最内层的承诺来解决,所有的承诺都代表了同样的结果。 当任务结束时,条件满足,最内层的承诺以实际值解决,所有这些承诺应该用相同的值解决。 这将最终以O(n)成本步行解决链(如果天真地执行,这甚至可以recursion完成,并导致堆栈溢出)。 之后,除最外面的所有承诺都可以成为垃圾收集。

相比之下,承诺链是由类似的东西构build的

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

会出现尖峰,同时分配n承诺对象,然后逐一解决,垃圾收集前一个,直到最终的承诺还活着。

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

这是吗?

不必要。 如上所述,所有批量中的承诺最终都以相同的值2来解决,所以我们只需要一次存储最外层和最内层的承诺。 所有中间的承诺可能会尽快被垃圾回收,我们希望在恒定的空间和时间内运行这个recursion。

事实上,这个recursion构造对于具有dynamic条件的asynchronous循环 (没有固定数量的步骤)是完全必要的,你不能真正避免它。 在Haskell中, IO monad始终使用这个函数,因此仅仅因为这种情况才对其进行优化。 这与尾调用recursion非常相似,这通常被编译器消除。

有没有人以这种方式考虑过构build连锁店的内存问题?

是。 例如, 在promises / aplus上讨论了这个问题 ,尽pipe没有结果。

许多承诺库支持迭代助手,以避免连锁的承诺,如蓝鸟的eachmap方法。

我自己的承诺库3没有引入内存或运行时间开销的特性parsing链。 当一个承诺采用另一个承诺(即使仍然待定)时,它们变得难以区分,中间承诺不再被引用到任何地方。

内存消耗会有所不同吗?

是。 虽然这种情况可以优化,但很less。 具体而言,ES6规范确实要求Promise在每次resolve调用时检查值,因此折叠链是不可能的。 链中的承诺甚至可以用不同的价值来解决(通过构build滥用获取者的示例对象,而不是现实生活中的)。 这个问题在esdiscuss提出,但仍然没有得到解决。

所以,如果你使用泄漏的实现,但需要asynchronousrecursion,那么你最好切换callback,并使用延迟反模式将最内层的promise结果传播给一个结果promise。

[1]:没有官方的术语
[2]:好,他们互相解决。 但我们希望以相同的价值来解决它们,我们期望这一点
[3]:无证游乐场,通过aplus。 阅读代码自己的危险: https : //github.com/bergus/F-Promise

免责声明:过早优化是不好的,找出性能差异的真正方法是基准你的代码 ,你不应该担心这(我只有一次,我已经使用了至less100个项目的承诺) 。

这是吗?

是的 ,承诺必须“记住”他们所遵循的是什么,如果你为10000个承诺做了这样的承诺,那么你将有一个10000长的承诺链,否则你不会(例如recursion) – 对于任何排队stream量控制都是如此。

如果你必须跟踪10000个额外的事情(操作),那么你需要保持记忆,这需要时间,如果这个数字是一百万,它可能是不可行的。 这在图书馆之间是不一样

有没有人以这种方式考虑过构build连锁店的内存问题?

当然,这是一个很大的问题,在像蓝鸟这样的库中使用诸如Promise.each类的东西, then才能进行链接。

我个人曾经在我的代码中避免使用这种风格的快速应用程序来遍历虚拟机中的所有文件 – 但在绝大多数情况下,这不是问题。

内存消耗会有所不同吗?

是的,很好。 例如,如果bluebird 3.0检测到承诺操作已经是asynchronous的(例如,如果它以Promise.delay开始),并且只是同步执行(因为asynchronous保证已经保存),就不会分配额外的队列。

这意味着我在第一个问题的答案中声明的并不总是正确的(但在正常使用情况下是这样)。除非提供内部支持,否则原生承诺将永远无法执行此操作。

然后再一次 – 承诺库彼此之间差别很大,这并不奇怪。

为了补充真棒现有的答案,我想说明这种asynchronousrecursion的结果expression式。 为了简单起见,我使用一个简单的函数来计算给定的基数和指数的功率。 recursion和基本情况等同于OP的例子:

 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} 

在一些替代步骤的帮助下,recursion部分可以被replace。 请注意,这个expression式可以在你的浏览器中进行评估:

 // 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} 

解释:

  1. 使用new Promise(res => setTimeout(res, 0, 8)) 0,8 new Promise(res => setTimeout(res, 0, 8)) ,执行程序立即被调用并执行一个非locking计算(用setTimeout模拟)。 然后返回一个未解决的Promise 。 这相当于OP示例的doSomethingAsync()
  2. parsingcallback通过.then(...Promise关联。注意:这个callback的主体被powerp的主体powerp
  3. 点2)被重复,并build立一个嵌套的处理程序结构,直到达到recursion的基本情况。 基本情况返回一个parsing为1Promise
  4. 嵌套的处理程序结构通过相应地调用关联的callback来“解开”。

为什么生成的结构是嵌套的而不是链接的? 因为在处理程序中的recursion情况阻止它们返回值,直到达到基本情况。

如何在没有堆栈的情况下工作? 相关的callback形成一个“链”,桥接主事件循环的连续微任务。

我刚刚发现了一个可能有助于解决问题的黑客攻击:不要在最后一次做recursion,而应该在最后一次catch进行recursion,因为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 } 

这个承诺模式将会产生一个recursion链。 所以,每一个resolve()都会创build一个新的栈帧(有自己的数据),利用一些内存。 这意味着使用这种承诺模式的大量链接函数会产生堆栈溢出错误。

为了说明这一点,我将使用一个名为Sequence的小型承诺库 ,我写了这个库 。 它依靠recursion实现链式函数的顺序执行:

 var funcA = function() { setTimeout(function() {console.log("funcA")}, 2000); }; var funcB = function() { setTimeout(function() {console.log("funcB")}, 1000); }; sequence().chain(funcA).chain(funcB).execute(); 

序列适用于中小型连锁店,function范围为0-500。 但是,在大约600条链中,序列开始降级并经常产生堆栈溢出错误。

底线是: 目前 ,基于recursion的诺言库更适合于小型/中型函数链,而基于约简的诺言实现适用于所有情况,包括较大的链。

这当然不意味着基于recursion的承诺是不好的。 我们只是需要考虑到他们的局限性。 另外,你很less会需要通过承诺连接那么多的电话(> = 500)。 我通常发现自己使用他们的asynchronousconfiguration,利用沉重的阿贾克斯。 但即使最复杂的情​​况下,我也没有看到超过15链的情况。

在一个侧面说明…

这些统计数据是从我的另一个库( provisnr)执行的testing中获得的,这些数据库捕获了在给定的时间间隔内实现的函数调用次数。