【问题标题】:Why such recursion not getting stack-overflowed?为什么这样的递归没有堆栈溢出?
【发布时间】:2019-11-18 03:14:42
【问题描述】:

我不明白为什么调用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 上运行这个sn-ps:

const recSetTimeOut = () => { setTimeout(recSetTimeOut, 0); }
recSetTimeOut();

结果Node:没有错误。

结果Chrome:没有错误。

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

结果Node:

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

结果Chrome:浏览器崩溃。

【问题讨论】:

  • 赞成使堆栈溢出成为动词。
  • 无递归。 “递归”recSetTimeout 调用实际上是预定的,并且不是当前函数堆栈的一部分。每次回调,栈都是干净的,只有调度器在栈顶。
  • @Amy,Promise 也不是,对吧? Promise 总是异步的?
  • 您可以通过发出异常并检查堆栈跟踪来验证这一点。
  • 如前所述,我认为这在任何时候都不会产生堆栈溢出。事情变慢了,但那是因为你不断地运行代码,调度程序、GC 等需要跟上。如果您看到 actual 堆栈溢出消息,请记住将其添加到您的问题中。如果不是:最好提及您所看到的,而不是提及堆栈溢出。

标签: javascript


【解决方案1】:

让我们依次看看。

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 解决时,都会立即调用 then 处理程序。完成后,将立即调用 then 处理程序。完成后... UI 线程将饿死,并且永远不会处理 UI 事件。与第一种情况一样,调用堆栈不会增长,因为每个回调都是由有效的循环进行的。这称为“承诺链”。如果一个 Promise 解析为一个 Promise,那么新的 Promise 就会被调用,这不会导致堆栈增长。但是,它所做的 是阻止 UI 线程做任何事情。

  • 实际上是一个无限循环。
  • 拒绝将控制权交还给 UI。
  • 调用堆栈没有增长。
  • 下一个调用会立即调用,并且带有极大的偏见。

您可以使用console.log((new Error()).stack) 确认两个堆栈跟踪实际上都是空的。

这两种解决方案都不应该导致堆栈溢出异常,尽管这可能取决于实现;浏览器的调度程序可能与 Node 的运行方式不同。

【讨论】:

  • 我不确定我是否遵循,为什么setTimeout 不会饿死 UI 线程,因为它也在以浏览器可以处理的速度执行您的处理程序?也许与您的陈述有关:The next call is invoked immediately and with extreme prejudice. 但我不确定如何...
  • @JuanMendes setTimeout 不会立即运行处理程序。它将其调度为延迟执行,并且调度程序在 UI 线程空闲之前不会出队并执行处理程序。
  • Promise handlers 也是调度的,就是异步运行的,我还是不跟。我可以通过运行这两个示例来证明这一点,您仍然会看到来自 setTimeout 的控制台消息
  • 如果您将第二个示例复制到浏览器控制台并运行它,该浏览器选项卡将停止响应 UI 事件。您可能可以滚动,但单击不会做任何事情。这是因为 UI 线程被有效地捆绑在一个无限循环中。 Promise 创建计划作业,但在与 setTimeout 和 setInterval 不同的队列中,并且该 Promise 计划程序不会给 UI 时间呼吸。
  • 我很想看到一个链接到指定这个的东西;否则,这些是依赖于实现的观察
【解决方案2】:

根据我对您的问题的理解,这是失败的,因为 then 不接受您所说的参数。

这样的事情可能会产生预期的结果......

const recPromise = async () => {
  return Promise.resolve(recPromise())
}

【讨论】:

  • 什么? .then 确实接受一个 callback,它是一个 argument。
  • developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… : The then() method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise.
  • 出于另一个原因。
  • 为什么回调名称很重要?这个答案或你的理由都不正确。对于本示例,已解决的承诺的值无关。
  • 没有抛出错误的代码并不能说明您对正在发生的事情的解释是否正确(不是。)
猜你喜欢
  • 1970-01-01
  • 2018-03-25
  • 2022-11-01
  • 2012-07-08
  • 1970-01-01
  • 1970-01-01
  • 2015-04-04
  • 2017-01-20
  • 2018-12-02
相关资源
最近更新 更多