使用setTimeout会阻止堆栈增长吗?

时间:2015-10-23 20:05:14

标签: javascript performance stack-overflow

让我们说我对一组价值观进行某种长期操作。

启动此操作的功能是startNext()

并且在其中执行的最后一行是自身,所以它是一个递归调用,如下所示:

function startNext(){
   var val = getNextValue()
   workOnValue(val)
      .then(doSomeMoreWork)
      .then(doMoreStuff)
      .then(moree)
      .then(startNext);
}

这将使堆栈增长,因为尾递归在JS(尚未)中不起作用。 将最后一行更改为:

.then(function(){setTimeout(startNext, 0)});

工作更好? 它是否会填充堆栈,因为它为事件循环添加了一个新操作?

2 个答案:

答案 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”,其中调用处理程序。

Promises A+