Nodejs中setTimeout和process.nextTick递归调用为什么不会栈溢出?
Nodejs中setTimeout和process.nextTick递归调用为什么不会栈溢出?
代码
// Generated by CoffeeScript 1.4.0
var a, f, log;
a = 1;
log = console.log;
f = function(b) {
log(b);
return process.nextTick(function() {
return f(b + 1);
});
};
f(a);
居然就这么执行了… 不是说 JS 没有尾递归优化的么,
而且作用域里的 b
不是也要保存的么… 可是没有栈溢出的报错
Node.js 中 setTimeout
和 process.nextTick
递归调用为什么不会栈溢出?
在 JavaScript 中,尤其是 Node.js 环境下,递归调用通常是通过函数栈实现的。如果递归调用过深,会导致栈溢出错误(RangeError: Maximum call stack size exceeded
)。然而,在 Node.js 中使用 setTimeout
和 process.nextTick
进行递归调用时,这种问题并不会出现。这是因为这些方法将后续操作推迟到事件循环的下一个阶段执行。
示例代码
// 生成的 CoffeeScript 代码
var a, f, log;
a = 1;
log = console.log;
f = function(b) {
log(b);
return process.nextTick(function() {
return f(b + 1);
});
};
f(a);
在这个例子中,函数 f
通过 process.nextTick
来递归调用自身。每次调用 f
时,都会打印当前的值 b
并将下一次调用推迟到事件循环的下一个 tick。因此,函数栈不会累积,从而避免了栈溢出。
解释
-
事件循环:Node.js 使用事件循环机制来处理异步操作。
setTimeout
和process.nextTick
都会将回调推迟到事件循环的下一阶段执行。 -
process.nextTick
:它将回调推迟到当前操作完成后立即执行。这使得回调可以在当前操作完成之后、但在任何 I/O 操作之前执行。这种方式不会增加函数调用栈,因为每次调用都是在事件循环的不同阶段进行的。 -
setTimeout
:类似于process.nextTick
,但它会将回调推迟到下一事件循环周期。这样可以确保每次回调都在新的周期中执行,从而避免栈溢出。
通过这种方式,setTimeout
和 process.nextTick
实现了一种非阻塞递归调用的方式,使得递归调用不会导致栈溢出。相反,它们会在事件循环的不同阶段执行,有效地避免了递归深度问题。
希望这个解释能帮助你理解为什么在 Node.js 中使用 setTimeout
和 process.nextTick
进行递归调用不会导致栈溢出。
如果不用process.nextTick
,为什么递归次数会出现以下结果:
1.如果是保存为文件,运行:永远是在17956次的时候停止?
2.如果是在node-shell里运行:则递归次数不固定,但会停止!
我是文件 17927
…
coffee
的 Shell 是 20902
…
在 node
里在这附近变动 20893
20888
20886
20883
…
v0.8.16
网上搜出来, 貌似和栈的深度有对应… 没测试 https://groups.google.com/forum/?fromgroups=#!topic/nodejs-dev/KyBoGTAq1cQ
$ node --stack_size=2048
> var err, n=0; (function f(){ n++; try {f()} catch (e) {err=e}})(), [err.message, n]
[ ‘Maximum call stack size exceeded’, 21809 ]
$ node --stack_size=4096
> var err, n=0; (function f(){ n++; try {f()} catch (e) {err=e}})(), [err.message, n]
[ ‘Maximum call stack size exceeded’, 43654 ]
$ node --stack_size=8192
> var err, n=0; (function f(){ n++; try {f()} catch (e) {err=e}})(), [err.message, n]
Segmentation fault ( oops :-)
这玩意不算真正的递归调用吧。执行一次,将内容压入任务队列中,下次循环的时候取出来执行,然后重复。并不是在一次循环中完成的。
就是说递归是不会压入队列了… 那除此之外还有区别么, 比如压入队列的环境的内容, 是跟直接递归时一样的?
这不是递归调用 你只是递归定义新函数而已
后续的 f
不是原先定义的那个 f
继续执行?
每次nexttick调用的匿名函数是新定义的,它在转调用的f,也就是你的f通过一个匿名函数调用f,中间间隔了一层,就不是递归调用了
意思能懂, 但让我自己区分还真是区分不出来了…
process.nextTick
到底做了什么…
在 Node.js 中,setTimeout
和 process.nextTick
都有自己的机制来避免栈溢出的问题。
-
process.nextTick
:process.nextTick
将回调函数放到一个特殊的队列中,在当前操作完成后立即执行。这意味着每次调用nextTick
时,都会清空当前的堆栈,并在下一个事件循环周期中处理这些回调函数。因此,即使递归调用很多次,也不会导致栈溢出。 -
setTimeout
:setTimeout
会将回调函数放入一个定时器队列中,直到指定的时间后才会被执行。这同样避免了递归调用直接增加调用栈的深度。
示例代码
let a = 1;
const log = console.log;
function f(b) {
log(b);
process.nextTick(() => {
f(b + 1);
});
}
f(a);
在这个例子中,虽然 f
函数递归调用了很多次,但由于每次调用 process.nextTick
后都会清空当前的调用栈,所以不会出现栈溢出的情况。
解释
- 每次
process.nextTick
被调用时,它会将回调函数加入到一个队列中。 - 在当前调用栈清空后,Node.js 会依次处理这些队列中的回调函数。
- 这种机制使得递归调用不会直接累加到调用栈中,从而避免了栈溢出的问题。