让我们说我对一组价值观进行某种长期操作。
启动此操作的功能是startNext()
并且在其中执行的最后一行是自身,所以它是一个递归调用,如下所示:
function startNext(){
var val = getNextValue()
workOnValue(val)
.then(doSomeMoreWork)
.then(doMoreStuff)
.then(moree)
.then(startNext);
}
这将使堆栈增长,因为尾递归在JS(尚未)中不起作用。 将最后一行更改为:
.then(function(){setTimeout(startNext, 0)});
工作更好? 它是否会填充堆栈,因为它为事件循环添加了一个新操作?
答案 0 :(得分:2)
是的,setTimeout
将阻止堆栈增长,因为当前函数已完成且"递归"对自身的调用将被置于事件队列中。它也会运行得更慢,因为它没有被直接调用,而是通过队列进行处理。
要演示并证明这一点,请尝试使用Node进行实验。
将下面的代码示例放入文件中,并将simple
标记切换到底部。您将看到recurseSimple
函数运行速度非常快,并且非常快速地打击堆栈。 recurseTimeout
运行速度较慢但会永远运行。
function recurseSimple(count) {
// Count: 15269, error: bootstrap_node.js:392
// RangeError: Maximum call stack size exceeded
try {
if (count % 10000 === 0) {
console.log('Running count:', count);
}
recurseSimple(count + 1);
} catch (e) {
console.log(`Simple count: ${count}, error:`, e);
}
}
function recurseTimeout(count) {
// No stack exceeded
try {
if (count % 10000 === 0) {
console.log('Running count:', count);
}
setTimeout(recurseTimeout.bind(null, count + 1), 0);
} catch (e) {
console.log(`Timeout count: ${count}, error:`, e);
}
}
const simple = false;
if (simple) {
recurseSimple(0);
} else {
recurseTimeout(0);
}
完全相同的原则适用于承诺。我没有在这里使用承诺,以保持尽可能简单。
答案 1 :(得分:1)
then
处理程序被推出执行上下文堆栈,因此它已经按照您的建议执行:
在执行上下文堆栈仅包含平台代码之前,不得调用onPulfilled或onRejected。 [3.1]。
这适用于A +承诺。
为清晰起见,这是3.1注释:
此处“平台代码”表示引擎,环境和承诺实现代码。在实践中,此要求确保onFulfilled和onRejected异步执行,然后在调用事件循环之后执行,并使用新堆栈。这可以使用“宏任务”机制(如 setTimeout 或 setImmediate )或“微任务”实施“机制,例如 MutationObserver 或 process.nextTick 。由于promise实现被认为是平台代码,它本身可能包含一个任务调度队列或“trampoline”,其中调用处理程序。