我正在编写一个处理大量数据的测试。令我惊讶的是,如果我在函数中添加 setTimeout,它将不再导致堆栈溢出(对于这个网站来说多么合适)。
这怎么可能,代码看起来真的是递归的。
每个 setTimeout 调用都会创建自己的堆栈吗?
有没有办法在不增加所需内存的情况下实现这种行为(异步且按顺序处理巨大的数组/数字)?
function loop(
left: number,
callbackFunction: (callback: () => void) => void,
) {
if (left === 0) {
return
}
console.log(left)
callbackFunction(() => {
loop(left - 1, callbackFunction)
})
}
function setTimeoutCallback(callback: () => void) {
setTimeout(
() => {
callback()
},
Math.random() * 5
)
}
function nonSetTimeoutCallback(callback: () => void) {
callback()
}
loop(100000, setTimeoutCallback) //no stack overflow
loop(100000, nonSetTimeoutCallback) //stack overflow
因为它不再是递归的。至少在技术上不是。
源代码看起来确实是递归的,因此程序员可能会编写这样的代码,就好像它是递归的一样,但从 CPU 的角度来看,它不再是递归的。它在循环中按顺序处理。
递归和堆栈
递归函数调用自身。当这种情况发生时,堆栈会不断增加,直到最后一个函数返回。在函数返回之前,函数的堆栈帧不会从堆栈中删除(现在让我们忽略闭包),因此,因为递归函数调用自身,所以在对自身的调用返回之前,它不会返回。这就是导致堆栈增长的原因。
尾递归
Lisp、Haskell 和 Scala 等语言认识到,在某些情况下,在进行递归时可以释放堆栈帧。一般来说,如果递归调用是函数中的最后一条指令,并且没有对返回值进行其他处理,则可以删除当前堆栈帧,因为在递归函数返回后将不再使用它。因此,此类语言实现了所谓的尾递归:在不增加堆栈的情况下无限递归的能力。
这对于非常纯粹的函数式语言特别有用,在这种语言中,唯一的编程结构就是函数,因为如果没有语句的存在,就不可能有循环语句或条件语句等。尾递归使得 Lisp 中的无限循环成为可能。
然而,Javascript 没有尾递归。所以这不会影响 Javascript 中递归的行为方式。我提到这一点是为了注意并非所有递归都需要增加堆栈。
调度
定时器功能如setTimeout()
and setInterval()
不调用传递给它们的函数。不仅没有立即给他们打电话,甚至根本就没有给他们打电话。他们所做的就是将函数以及何时调用该函数的信息传递给事件循环。
事件循环本质上是javascript的核心。当且仅当没有更多的 javascript 需要执行时,解释器才会进入事件循环。您可以将事件循环视为解释器的空闲状态。事件循环不断检查事件(I/O、UI、计时器等)并执行附加到事件的相关函数。这是您传递给的函数setTimeout()
.
设置超时时间
因此,根据上面给出的事实,我们可以看到“递归”是如何通过setTimeout
并不是真正的递归。
首先你的函数调用setTimeout
并将其自身传递给它。
setTimeout
将函数引用保存到事件侦听器列表中,并设置计时器来触发将触发函数的事件
您的函数继续并返回,请注意“递归”函数尚未被调用。由于您的函数返回,因此它的堆栈帧会从堆栈中删除.
JavaScript 进入事件循环(不再需要处理 JavaScript)。
函数的计时器到期,事件循环调用它。重复直到停止呼叫setTimeout
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)