含setTimeout的异步“递归”:调用栈是否增长?trace变长原因
问题解答
先看你给出的两段代码:
带 setTimeout 的异步递归版本
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 的版本并没有增长调用栈,两个版本调用踪迹(trace)变长的原因完全不同:
同步递归版本的调用栈确实在增长:
这是典型的同步递归调用,每次执行x(n+1)都是在当前x(n)的执行过程中直接发起调用,新的函数执行上下文会压入当前调用栈。比如执行x(0)时调用x(1),x(1)调用x(2),x(2)调用x(3),此时调用栈层级是x(3) → x(2) → x(1) → x(0),所以console.trace()输出的调用链会逐次变长,这是真实的调用栈堆叠。带 setTimeout 的版本是异步溯源记录导致 trace 变长:
这个版本的递归是异步触发的:当执行到await setTimeout(...)时,x(n)会暂停执行并退出当前调用栈;1秒后,浏览器事件循环触发 setTimeout 的回调函数,此时才会调用x(n+1)——这个x(n+1)是在全新的调用栈里执行的,和之前的x(n)调用栈没有直接关联。之所以看起来 trace 在变长,是因为浏览器开发者工具的异步栈追踪功能:为了方便调试异步代码,工具会把触发当前异步操作的上游上下文也记录下来,形成一条完整的异步调用溯源链。但这不是实际运行时的调用栈,你可以通过打印调用栈长度验证:比如在函数里加入
console.log(new Error().stack.split('\n').length),会发现每次的调用栈长度基本一致,并没有逐次增长。
内容的提问来源于stack exchange,提问作者Danny
相关产品推荐
相关产品推荐

