求科普 setImmediate API
发布于 11 年前 作者 jiyinyiyong 20480 次浏览 最后一次编辑是 8 年前

看到 @PeakJi](http://weibo.com/peakji) 一条微博

node.js 0.9之后,任何异步递归都应用setImmediate而非process.nextTick!setImmediate和process.nextTick的区别是:前者将defer到队列末,且不会生成call stack;而后者是defer到该函数结束后执行,且process.nextTick用于递归会报警并最后爆盏…至于setInterval(func,0)就别用了,那是浏览器技巧。

地址: http://weibo.com/1811148004/zxJaEh6jK

觉得这个 API 好陌生… 搜索到的东西不多… https://github.com/NobleJS/setImmediate 可以细化一下么?

16 回复

就字面上理解,就是说 setImmediate 是把那个调用放到了队列中去,而 process.nextTick 并没有把调用放到队列中,只是保存在了某个地方,等待调用 process.nextTick 的那个函数结束后,再自动执行。

忽略我…

call stack 和性能呢?

个人认为,process.nextTick 是一种插队行为,一种藐视规则的行为,应加以谴责。性能上我认为没有本质区别。

这里说的setImmediate不是指Github上那个setImmediate.js,而是一个新的API。 你可以在最新版本的node下试试如下的代码:

function recurse(i,end){
if(i>end)
{
	console.log('Done!');
}
else
{
	console.log(i);
	process.nextTick(recurse(i+1,end));
}

}

recurse(0,99999999);

执行几次后马上就 RangeError: Maximum call stack size exceeded

然后换用setImmediate:

function recurse(i,end){
if(i>end)
{
	console.log('Done!');
}
else
{
	console.log(i);
	setImmediate(recurse,i+1,end);
}

}

recurse(0,99999999);

就完全没问题!

这是因为setImmediate不会生成call stack,异步的递归必须用它。

性能上差不多,关键差别在于是否有call stack,见我下面的代码~

难道是尾递归优化相似的作用么? 不过只有 IE 部署的 API 真让人觉得怪怪的 https://developer.mozilla.org/en-US/docs/Web/API/window.setImmediate Node 也到 0.10.0 才有这个 API… http://stackoverflow.com/questions/15349733/setimmediate-vs-nexttick

我还不是太说的清楚他们之间的区别,不过我现在看到一篇文章是讲 nextTick 的。里面有提到 call stack 之类的东西。

我认为说的不正确,无论是setImmediate还是process.nextTick的递归调用都不会造成栈溢出,如果是同步的递归,必须保存调用栈,而异步的递归根本不存在调用栈。之所以会发生Maximum call stack size exceeded,因为process.maxTickDepth的缺省值是1000,如果递归调用nextTick只能调用1000次,超过1000就会报这个错,但并不是真正栈溢出,只是想给你一个提示不希望你递归调用nextTick太多次,如果nextTick递归调用,那么其他的回调事件就会等待,会造成event loop饥饿,所以官方推荐用setImmediate作为递归调用,为什么setImediate,nextTick一个造成不饥饿,一个造成饥饿呢?那就要说两者的区别,可以看我的另一个回答。

首先同意@nodehugo所说,nextTick的栈溢出并不是真正的栈溢出,而是人为加上的东西,当初的讨论在这里 https://github.com/joyent/node/pull/3709https://github.com/joyent/node/pull/3723

抛却maxTickDepth的问题,nextTick 和 setImmediate 主要的区别在于任务插入的位置

nextTick 的插入位置是在当前帧的末尾、io回调之前,如果nextTick过多,会导致io回调不断延后 setImmediate 的插入位置是在下一帧,不会影响io回调

我这人最大的问题就是看不得格式混乱,好吧,帮你整理了下 第一段:

function recurse (i, end) {
  if (i > end) {
    console.log('Done!');
  } else {
    console.log(i);
    process.nextTick(recurse(i+1, end));
  }
}

第二段;

function recurse (i, end) {
  if (i > end) {
    console.log('Done!');
  } else {
    console.log(i);
    setImmediate(recurse, i+1, end);
  }
}

可以这样来理解. process.nextTick和setImmediate方法在实现上分别对应这两个队列-- 不妨叫作nextTick队列和immediate队列. 到nextTick的时候, 这两个队列被执行的情况有所差异: nextTick队列中的方法会被全部执行(包括在执行过程中新加的),而immediate队列只会取其第一个方法来执行.

function nextTick(msg, cb) { process.nextTick(function() { console.log('tick: ’ + msg); if (cb) { cb(); } }); }

function immediate(msg, cb) { setImmediate(function() { console.log('immediate : ’ + msg); if (cb) { cb(); } }); }

nextTick(‘1’); nextTick(‘2’, function() { nextTick(‘10’); });

immediate(‘3’, function() { nextTick(‘5’); });

nextTick(‘7’, function() { immediate(‘9’); });

immediate(‘4’, function() { nextTick(‘8’); });

这段代码的输出是:

tick: 1 tick: 2 tick: 7 tick: 10 immediate : 3 tick: 5 immediate : 4 tick: 8 immediate : 9

解释如下: 1). 第一遍执行时, 会分别向nextTick队列和immedidate队列中加入方法,它们变成: nextTick: 1 2 7 数字代表输出相应数字的那个nextTick方法对应的callback方法,下同 immedidate: 3 4 2). 到了nextTick, 开始执行回调 先执行 nextTick 队列中的回调(全部执行才结束): 2.1) 执行1 – 输出1, nextTick队列变为 2 7 2.2) 执行 2 – 输出2, 并向nextTick队列添加10, nextTick队列变为 7, 10 2.3) 执行7 – 输出7, 并向immediate队列添加9. nextTick 队列变为 10, immediate队列变为 3 4 9 2.4) nextTick 队列中还有一个新添加的10, 故执行它, 输出10

再执行 immediate队列的第一个回调方法(immediate队列为 3 4 9), 即执行3. 3的执行,输出3同时向nextTick队列中加入5 — 5 不会在这个tick执行, 因为本轮nextTick的执行已经结束了. 此时, 队列变为: nextTick: 5 immedidate: 4 9 这也是进入下一轮tick前的队列状态.

3). 到了nextTick, 开始执行回调 队列为: nextTick: 5 immedidate: 4 9

和上一轮类似: 3.1) 执行 nextTick 5, 输出 5. nextTick为空, 执行完毕.

执行immediate队列的第一个回调, 即4, 输出4, 并向nextTick队列加入8. 此时队列变化为:
nextTick: 8 immediate: 9

4). 到了nextTick, 开始执行回调 根据上述原理,不难知道 将分别执行 nextTick 8 和 immediate 9 , 将输出8输出9.

setImmediate会让步io事件先执行,nextTick则不会。 如果你不清楚它们的区别是什么的时候,你可以不考虑nexttick。

nextTick的递归写法有问题,用process.nextTick(recurse(i+1,end));的话相当于直接执行recurse(i+1,end),也就变成了普通的函数递归,因此会爆栈,如果改为以下写法就没有这个问题,在0.10版本之前不会出现爆栈问题 process.nextTick(function() { return recurse(i+1, end); }); 而在0.10版本后由process.maxTickDepth控制递归次数

@GuoZhang 很不错的例子,相当赞。

试了下,在node 0.10.28下,输出跟上面例子里提到的结果相同。而在 node 0.12.2 里,输出结果不同,莫非特性有变化?

tick: 1 tick: 2 tick: 7 tick: 10 immediate : 3 immediate : 4 immediate : 9 tick: 5 tick: 8

@chyingp 的确发生了变化,可以参考最新的文档。

下面是对GuoZhang 的代码的扩展(node v0.12.7):

'use strict';

function nextTick(msg, cb) {
  process.nextTick(function() {
    console.log('tick: ' + msg);
    if (cb) {
      cb();
    }
  });
}

function immediate(msg, cb) {
  setImmediate(function() {
    console.log('immediate : ' + msg);
    if (cb) {
      cb();
    }
  });
}

nextTick('1');
nextTick('2', function() {
nextTick('10');
});

immediate('3', function() {
  nextTick('5');
});

nextTick('7', function() {
immediate('9');
});

immediate('4', function() {
  nextTick('8');
});

let n = 0;

const timer = setInterval(function() {
  n++;
  console.log('interval:', n);
  nextTick('tick from interval: ' +  n);
  nextTick('another tick from interval: ' +  n);
  immediate('immediate from interval: ' + n);
  immediate('another immediate from interval: ' + n);

  if ( n === 3 ) {
    clearInterval(timer);
  }
}, 0);

console.log('the last line of the program.');
回到顶部