为什么这样的递归没有堆栈溢出?

9

我无法理解为什么调用recSetTimeOut()不会导致堆栈溢出错误,而recPromise()会。

const recSetTimeOut = () => {
  console.log('in recSetTimeOut');
  setTimeout(recSetTimeOut, 0)
};
recSetTimeOut();

const recPromise = () => {
  console.log('in recPromise');
  Promise.resolve().then(recPromise);
}
recPromise();

为什么会发生这种情况?它们之间有什么区别?
你能解释一下背后的过程吗?
编辑增加了更多信息
在Node.js v12.1.0和Chrome DevTools上运行这些片段:
const recSetTimeOut = () => { setTimeout(recSetTimeOut, 0); }
recSetTimeOut();

结果 Node: 没有错误。

结果 Chrome: 没有错误。

const recPromise = () => { Promise.resolve().then(recPromise); }
recPromise();

结果 Node:

致命错误:表格大小无效 分配失败 - JavaScript堆内存不足

结果 Chrome:

浏览器崩溃。


2
没有递归。 "递归"的recSetTimeout调用实际上是被安排的,不是当前函数堆栈的一部分。每次回调时,堆栈都是干净的,只有调度程序位于堆栈顶部。 - user47589
1
@Amy,Promise也不是异步的吧?Promise总是异步的吗? - Ruan Mendes
2
您可以通过引发异常并检查堆栈跟踪来验证此内容。 - user47589
2
正如所述,我根本没有看到这会在任何时候引发堆栈溢出。事情变慢了,但那是因为你不断地运行代码,调度程序、GC等需要赶上来。如果您看到了一个实际的堆栈溢出消息,请记得将其添加到您的问题中。如果没有:最好提到您看到的内容,而不是提到堆栈溢出。 - Mike 'Pomax' Kamermans
显示剩余7条评论
2个回答

6

让我们依次看一下。

const recSetTimeOut = () => {
  console.log('in recSetTimeOut');
  setTimeout(recSetTimeOut, 0)
};
recSetTimeOut();

这实际上并不是递归。您正在使用调度程序注册recSetTimeOut。当浏览器的UI线程空闲时,它将从列表中取出下一个等待的函数,并调用它。调用堆栈永远不会增长;调度程序(本机代码)始终位于非常短的调用堆栈的顶部。您可以通过发出异常并检查其调用堆栈来验证此操作。

  • 该函数实际上并不是递归的,堆栈不会增长。
  • 每个调用后它都会将控制权返回给UI线程,从而允许处理UI事件。
  • 仅在UI完成其操作并调用下一个调度任务后,下一个调用才会发生。
const recPromise = () => {
  console.log('in recPromise');
  Promise.resolve().then(recPromise);
}
recPromise();

这实际上是一个无限循环,拒绝将控制权交还给用户界面(UI)。每次Promise解析(resolve)时,then处理程序(handler)会立即调用。当它完成后,另一个then处理程序就会立即调用。当这个操作完成时...UI线程将饿死,UI事件将永远不会被处理。与第一种情况类似,调用栈并没有增长,因为每个回调都由实际上是循环的内容发出。这称为"Promise链接(chaining)"。如果Promise解析为Promise,则会调用新的Promise,但这不会导致堆栈增长。然而,它确实阻止了UI线程执行任何操作。

  • 实际上是一个无限循环。
  • 拒绝将控制权交还给用户界面(UI)。
  • 调用栈不会增长。
  • 下一个调用会立即且毫不客气地被调用。

您可以使用console.log((new Error()).stack)来确认两个堆栈跟踪(stack trace)几乎为空。

虽然这可能取决于实现方式,但两种解决方案都不应导致堆栈溢出异常(Stack Overflow exception); 浏览器的调度程序可能与Node的不同。


@JuanMendes setTimeout 不会立即运行处理程序。它将其安排为延迟执行,并且调度程序直到 UI 线程空闲后才会取消队列并执行处理程序。 - user47589
Promise处理程序也是异步调度的,我仍然不明白。我可以通过运行这两个示例来证明它,您仍将看到来自setTimeout的控制台消息。 - Ruan Mendes
如果您将第二个示例复制到浏览器控制台并运行它,该浏览器选项卡将停止响应UI事件。您可能可以滚动,但单击不会有任何作用。这是因为UI线程在无限循环中被有效地占用。Promises创建计划的作业,但在与setTimeoutsetInterval不同的队列中,并且该Promise调度程序不会给UI时间来呼吸。 - user47589
为什么JavaScript Promise的.then()处理程序会在其他代码之后运行,这可能是你需要的。 - user47589
可能是这样,但MDN的这个部分似乎更清楚地表明了它:“传入的函数不会立即运行,而是被放在微任务队列中,这意味着它会在 JavaScript 事件循环的当前运行结束时稍后运行,也就是很快:”在 JavaScript 循环的当前运行结束时,我认为这与您所说的相匹配。堆栈完全展开,但 JavaScript 获得了另一个回合,而不是让 UI 做任何事情。 - Ruan Mendes
显示剩余2条评论

-6
根据我对你问题的理解,这个失败是因为then不接受参数,而你正在调用它。
像这样做可能会产生预期的结果...
const recPromise = async () => {
  return Promise.resolve(recPromise())
}

4
什么?.then接受一个回调函数作为参数 - user47589
2
then() 方法返回一个 Promise。它最多可以接受两个参数:用于 Promise 成功和失败情况的回调函数。 - user47589
1
出于另一个原因。 - user47589
3
回调函数的名称为什么很重要?这个答案或你的原因都是错误的。对于这个示例,已解决的承诺值在任何方面都不重要。 - Clint
1
代码没有抛出错误并不能说明你对发生的事情的解释是正确的(实际上并不正确)。 - JJJ
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接