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 不是也要保存的么… 可是没有栈溢出的报错


11 回复

Node.js 中 setTimeoutprocess.nextTick 递归调用为什么不会栈溢出?

在 JavaScript 中,尤其是 Node.js 环境下,递归调用通常是通过函数栈实现的。如果递归调用过深,会导致栈溢出错误(RangeError: Maximum call stack size exceeded)。然而,在 Node.js 中使用 setTimeoutprocess.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 使用事件循环机制来处理异步操作。setTimeoutprocess.nextTick 都会将回调推迟到事件循环的下一阶段执行。

  • process.nextTick:它将回调推迟到当前操作完成后立即执行。这使得回调可以在当前操作完成之后、但在任何 I/O 操作之前执行。这种方式不会增加函数调用栈,因为每次调用都是在事件循环的不同阶段进行的。

  • setTimeout:类似于 process.nextTick,但它会将回调推迟到下一事件循环周期。这样可以确保每次回调都在新的周期中执行,从而避免栈溢出。

通过这种方式,setTimeoutprocess.nextTick 实现了一种非阻塞递归调用的方式,使得递归调用不会导致栈溢出。相反,它们会在事件循环的不同阶段执行,有效地避免了递归深度问题。

希望这个解释能帮助你理解为什么在 Node.js 中使用 setTimeoutprocess.nextTick 进行递归调用不会导致栈溢出。


如果不用process.nextTick,为什么递归次数会出现以下结果: 1.如果是保存为文件,运行:永远是在17956次的时候停止? 2.如果是在node-shell里运行:则递归次数不固定,但会停止!

我是文件 17927coffee 的 Shell 是 20902… 在 node 里在这附近变动 20893 20888 20886 20883v0.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 中,setTimeoutprocess.nextTick 都有自己的机制来避免栈溢出的问题。

  • process.nextTickprocess.nextTick 将回调函数放到一个特殊的队列中,在当前操作完成后立即执行。这意味着每次调用 nextTick 时,都会清空当前的堆栈,并在下一个事件循环周期中处理这些回调函数。因此,即使递归调用很多次,也不会导致栈溢出。

  • setTimeoutsetTimeout 会将回调函数放入一个定时器队列中,直到指定的时间后才会被执行。这同样避免了递归调用直接增加调用栈的深度。

示例代码

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 会依次处理这些队列中的回调函数。
  • 这种机制使得递归调用不会直接累加到调用栈中,从而避免了栈溢出的问题。
回到顶部