在this answer中,递归建立了一个承诺链。
稍微简化,我们有:
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
}
据推测,这会产生一个调用堆栈和一个承诺链 - 即" deep"和"宽"。
我预计内存峰值会大于执行递归或单独建立一个promise链。
答案 0 :(得分:42)
调用堆栈和承诺链 - 即“深”和“宽”。
实际上,没有。这里没有承诺链,因为我们从doSomeThingAsynchronous.then(doSomethingAsynchronous).then(doSomethingAsynchronous).…
知道它(如果以这种方式编写的话,Promise.each
或Promise.reduce
可以执行处理程序。{/ p>
我们在这里面临的是一个解析链 1 - 当满足递归的基本情况时,最终会发生什么,就像{{1 }}。这只是“深度”,而不是“宽”,如果你想称之为。
我预计内存峰值会大于执行递归或单独建立一个promise链。
实际上不是尖峰。随着时间的推移,你会慢慢地构建大量的承诺,这些承诺用最里面的一个解决,所有的承诺都代表相同的结果。在任务结束时,条件得到满足并且最内层的承诺用实际值解决时,所有这些承诺都应该用相同的值来解决。这最终会导致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())
个promise对象,然后逐个慢慢解析它们,垃圾收集前一个,直到只有已确定的最终保证为止。
n
是这样吗?
不一定。如上所述,该批量中的所有承诺最终都使用相同的值 2 来解决,因此我们所需要的只是一次存储最外层和最内层的承诺。所有中间承诺可能会尽快被垃圾收集,我们希望在恒定的空间和时间内运行此递归。
事实上,对于具有动态条件(没有固定步数)的asynchronous loops,这个递归构造是完全必要的,你无法真正避免它。在Haskell中,memory
^ resolve promise "then" (tail)
| chain chain recursion
| /| |\
| / | | \
| / | | \
| ___/ |___ ___| \___ ___________
|
+----------------------------------------------> time
monad始终使用它,因此仅针对此情况实现了对它的优化。它与tail call recursion非常相似,编译器通常会将其删除。
有没有人考虑过以这种方式建立连锁店的记忆问题?
是。例如,这是discussed at promises/aplus,但尚无结果。
许多promise库都支持迭代助手,以避免promise IO
链的峰值,如Bluebird的then
和each
方法。
我自己的promise库 3,4 确实具有解析链,但不会引入内存或运行时开销。当一个承诺采用另一个承诺时(即使仍然悬而未决),它们变得难以区分,中间承诺不再在任何地方被引用。
承诺库之间的内存消耗会有所不同吗?
是。虽然这种情况可以优化,但很少。具体来说,ES6规范确实要求Promises在每次map
调用时检查值,因此无法折叠链。链中的承诺甚至可以用不同的值来解决(通过构造滥用getter的示例对象,而不是在现实生活中)。问题was raised on esdiscuss但仍未解决。
因此,如果您使用泄漏实现,但需要异步递归,那么最好切换回回调并使用deferred antipattern将最内层的promise结果传播到单个结果承诺。
[1]:没有官方术语
[2]:好吧,他们彼此解决了。但我们希望以相同的价值解决它们,我们期待那个
[3]:无证操场,通过aplus。阅读代码需要您自担风险:https://github.com/bergus/F-Promise
[4]:也在this pull request
答案 1 :(得分:15)
免责声明:过早优化很糟糕,了解性能差异的真正方法是对您的代码进行基准测试,您不应该担心这一点(我'我只需要一次,我已经使用了至少100个项目的承诺。)
是这样吗?
是,承诺必须“记住”他们所关注的内容,如果您为10000承诺执行此操作,您将拥有10000条长承诺链,如果您不这样做则不会(例如,使用递归) - 对于任何排队流控制都是如此。
如果你必须跟踪10000个额外的东西(操作),那么你需要为它保留内存,这需要时间,如果这个数字是一百万,它可能是不可行的。这在图书馆之间有所不同。
有没有人考虑过以这种方式建立连锁店的记忆问题?
当然,这是一个很大的问题,也是在Promise.each
能够链接的蓝鸟等库中使用类似then
的用例的用例。
我个人已经在我的代码中避免使用这种风格来快速浏览VM中的所有文件 - 但在绝大多数情况下,这都不是问题。
承诺库之间的内存消耗会有所不同吗?
是的,非常。例如,如果bluebird 3.0检测到promise操作已经异步(例如,如果它以Promise.delay开头)并且只执行一些事情,那么它将不会分配额外的队列同步(因为已经保留了异步保证)。
这意味着我在第一个问题的答案中声称并不总是正确的(但在常规用例中也是如此):)除非提供内部支持,否则本机承诺将永远无法执行此操作。
然后再次 - 这并不奇怪,因为承诺库彼此之间存在数量级的差异。
答案 2 :(得分: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
}
答案 3 :(得分:1)
为了补充令人敬畏的现有答案,我想说明表达式,这是这种异步递归的结果。为简单起见,我使用一个简单的函数来计算给定基数和指数的幂。递归和基本情况等同于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}
在一些替换步骤的帮助下,可以替换递归部分。请注意,可以在浏览器中评估此表达式:
// 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}
new Promise(res => setTimeout(res, 0, 8))
立即调用执行程序并执行非bllocking计算(使用setTimeout
模仿)。然后返回未解决的Promise
。这相当于OP示例中的doSomethingAsync()
。Promise
与此.then(...
相关联。注意:此回调的正文已替换为powerp
的正文。then
处理程序结构,直到达到递归的基本情况。基本案例返回使用Promise
解决的1
。then
处理程序结构是"解开"通过相应地调用相关的回调。为什么生成的结构是嵌套的而不是链接的?因为then
处理程序中的递归情况会阻止它们返回值,直到达到基本情况。
如果没有堆叠,这怎么办?相关的回调形成一个"链",它连接主事件循环的连续微任务。
答案 4 :(得分:0)
此承诺模式将生成递归链。因此,每个resolve()将使用一些内存创建一个新的堆栈帧(具有自己的数据)。这意味着使用此promise模式的大量链接函数会产生堆栈溢出错误。
为了说明这一点,我将使用我写过的小promise library called Sequence。它依赖于递归来实现链式函数的顺序执行:
var funcA = function() {
setTimeout(function() {console.log("funcA")}, 2000);
};
var funcB = function() {
setTimeout(function() {console.log("funcB")}, 1000);
};
sequence().chain(funcA).chain(funcB).execute();
序列适用于小型/中型链,范围为0-500个函数。但是,在大约600个链上,序列开始降级并且经常产生堆栈溢出错误。
底线是:当前,基于递归的promise库更适合中小型函数链,而基于reduce的promise实现适用于所有情况,包括更大的链。
这当然并不意味着基于递归的承诺是不好的。我们只需要考虑它们的局限性就可以使用它们。此外,您很少需要通过承诺链接那么多电话(> = 500)。我通常发现自己将它们用于使用大量ajax的异步配置。但即使是最复杂的情况,我还没有看到超过15个链条的情况。
旁注......
这些统计数据是从我的另一个库 - provisnr执行的测试中检索的 - 它捕获在给定时间间隔内实现的函数调用次数。