JavaScript: setTimeout "递归" - 如何检查堆栈是否增加?

3
给定函数
async function x(n) {
    console.log(n);
    console.trace();
    if (n >= 3) { return; };
    await setTimeout(() => x(n+1), 1000);
}

x(0);

我可以看到控制台中的跟踪信息越来越长。

与递归版本相比较,

async function x(n) {
    console.log(n);
    console.trace();
    if (n >= 3) { return; };
    x(n+1);
}

x(0);

你可以看到追踪也在增长。那么setTimeout是否也会增加堆栈,或者在追踪增长背后有什么解释呢?

栈会跟踪调用函数的内容,即使在使用setTimeout时也是如此。你能更具体地说明你的期望吗?就目前而言,我们无法回答除了“这是预期的行为”之外的任何其他问题。 - Kaddath
这两个函数都是递归的,我看不出任何区别。 - kevinSpaceyIsKeyserSöze
2
@kevinSpaceyIsKeyserSöze 第一个并不是真正的递归。有些人称之为伪递归 - Bergi
2
@kevinSpaceyIsKeyserSöze 第一个实际上并不是递归的。一些人称其为“伪递归”。 - Bergi
这个回答解决了你的问题吗?为什么在Chrome DevTools下使用setTimeout()会导致我的调用堆栈混乱? - Parzh from Ukraine
显示剩余5条评论
2个回答

2
不,堆栈在setTimeout版本中并没有增长。在console.trace()的输出中,你所看到的是在MDN文档中这样描述的:
注意:在某些浏览器中,console.trace()还可能输出导致当前console.trace()的调用序列和异步事件,这些事件不在调用堆栈上,以帮助确定当前事件评估循环的起源。
你可以在基于Chromium的浏览器中看到这一点,如果你不使用console.trace(),而是获取当前实际的堆栈。

async function x(n) {
  console.log(n);
  console.log(new Error().stack);
  if (n >= 3) {
    return;
  };
  await setTimeout(() => x(n + 1), 1000);
}

x(0);

这可能会因为其他浏览器的不同而产生不同的结果,因为.stack是一个非标准属性,但至少在Chromium中,它显示了从JavaScript事件循环触发的当前实际堆栈。

1
为什么你要等待 setTimeout? - kevinSpaceyIsKeyserSöze
1
你为什么在等待setTimeout? - kevinSpaceyIsKeyserSöze
1
为什么你在等待setTimeout? - undefined
1
@kevinSpaceyIsKeyserSöze 因为我复制/粘贴了原始代码 :) - James Thorpe
1
@kevinSpaceyIsKeyserSöze 因为我复制/粘贴了原始代码 :) - James Thorpe
显示剩余9条评论

1
在 setTimeout 和递归版本的函数中,控制台中的跟踪信息会不断增长,因为每次调用函数都会向调用栈添加一个新的条目。在递归版本中,调用栈会增长直到达到最大调用栈大小,并且程序会因为"超过最大调用栈大小"错误而崩溃。
在 setTimeout 版本中,调用栈不会无限增长,因为 setTimeout 函数会安排下一次对函数的调用在延迟1000毫秒后执行。这意味着每次调用函数都会在下一次调用执行之前完成并从调用栈中移除。然而,每次调用函数仍然会向调用栈添加一个新的条目,这就是为什么控制台中的跟踪信息会增长的原因。
要检查调用栈是否增长,可以使用 console.time() 和 console.timeEnd() 方法来测量每次调用函数完成所需的时间。如果每次调用之间的时间保持一致,说明调用栈没有增长。以下是一个示例:

async function x(n) {
  console.log(n);
  console.trace();
  if (n >= 3) {
    return;
  };
  const startTime = Date.now();
  await setTimeout(() => {
    console.timeEnd(`x(${n})`);
    x(n + 1);
  }, 1000);
  console.time(`x(${n + 1})`);
}
 console.time(`x(0)`);
x(0);


1
如果每次调用之间的时间保持一致,那意味着调用堆栈没有增长。 - 不完全是这样。 - Bergi
1
如果每个电话之间的时间间隔是一致的,那就意味着调用堆栈没有增长。 - 不完全是这样。 - Bergi
1
是的。函数调用通过固定数量增加堆栈,这是一个常数时间的操作。如果你无限递归地重复这个过程,调用堆栈仍然在增长,尽管每次调用所需的时间相同。其次,你的 console.time 正在测量 setTimeout,这太不准确了,无法对分配堆栈帧所需的纳秒数做出任何断言。 - Bergi
1
是的。函数调用通过固定的数量增加堆栈,这是一个常数时间的操作。如果你无限递归地重复这个过程,调用堆栈仍然会增长,尽管每个调用所需的时间相同。其次,你的 console.time 正在测量 setTimeout,这对于分配堆栈帧所需的纳秒级时间来说太不准确了。 - Bergi
1
是的。函数调用通过固定数量增加堆栈,这是一个常数时间操作。如果你无限递归地重复这个过程,调用堆栈仍然在增长,尽管每个调用所需的时间相同。其次,你的console.time正在测量setTimeout,这太不准确了,无法准确地说出分配堆栈帧所需的纳秒数。 - undefined
显示剩余6条评论

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