setImmediate与nextTick的区别

387
今天发布了Node.js版本0.10,并引入了setImmediate。根据API变更文档的建议,当进行递归的nextTick调用时,应使用它。
MDN所说的来看,它似乎与process.nextTick非常相似。
我何时应该使用nextTick,何时应该使用setImmediate

20
以下是关于这个变化的五段文字,取自 http://blog.nodejs.org/2013/03/11/node-v0-10-0-stable/:第一段: 我们很高兴地宣布 Node.js v0.10.0 版本已经发布,并且现在是稳定版。Node.js 是一个建立在 Google Chrome V8 JavaScript 引擎之上的平台,用于构建快速且可伸缩的网络应用程序。v0.10.0 是今天发布的最新版本,并且将是下一个长期支持(LTS)版本。第二段: 该版本的发布包括了许多增强功能和性能优化,以及对 Windows 的更好支持。其中最显著的改进之一是对流控制的改进,这为构建高吞吐量、低延迟的网络应用程序提供了更好的基础。第三段: 此外,该版本还包含了错误修复和安全性增强。特别是,我们致力于使 Node.js 成为一个安全的平台,并且已经修复了一些安全漏洞。第四段: 如果您已经在使用 Node.js,我们建议您升级到 v0.10.0 版本,以获取所有这些增强功能和改进性能。如果您正在考虑使用 Node.js,那么现在是开始尝试的好时机。第五段: 感谢社区中数千位贡献者的帮助和支持,使得此版本的发布成为可能。我们期待着您继续参与和支持 Node.js 的开发和使用。 - mak
1
从性能基准来看,在大型计算中,nextTicksetImmediate更快。 - user3644644
12
我先阅读了那五段落,但当问题没有真正解决时,我还是来到了这个问题。被接受的答案更为简洁,更详细地描述了setImmediate的功能。请注意,我的翻译尽可能通俗易懂,但不会改变原意。 - Chev
我在我的博客中详细解释了它们之间的区别。 - plafer
GC 可以在 setImmediate 之前运行,但不能在 nextTick 之前运行,这是正确的吗? - user663031
那是一个单独的问题,但不会 - 垃圾回收可以在任何时候运行。确实,setImmediate为GC提供了更适当的机会,在更清晰的范围(外部事件)上运行,但Node不会手动暂停GC以进行其他代码。顺便说一下,您始终可以使用--expose-gc自己运行GC或跟踪GC并查看 :) - Benjamin Gruenbaum
8个回答

564
如果您想在事件队列中排队函数,且已经有I/O事件回调,则使用setImmediate。如果要将函数有效地排队在事件队列头部,以便在当前函数完成后立即执行,则使用process.nextTick
因此,在试图使用递归来分解长时间运行的CPU限制作业的情况下,您现在应该使用setImmediate 而不是process.nextTick来排队下一次迭代,否则任何I/O事件回调都无法在迭代之间运行。

97
传递给process.nextTick的回调通常会在当前执行流结束时被调用,因此与同步调用函数几乎一样快。如果不加控制,这将使事件循环耗尽,防止发生任何I/O。 setImmediate按创建顺序排队,并在每个循环迭代中弹出队列一次。这与process.nextTick不同,后者将在每个迭代中执行process.maxTickDepth个排队的回调。 setImmediate在触发排队的回调之后会让出事件循环,以确保不会饿死I/O。 - Benjamin Gruenbaum
2
@UstamanSangat setImmediate仅受IE10+支持,所有其他浏览器都固执地拒绝实现可能的未来标准,因为它们不喜欢被微软打败。要在FF/Chrome中实现类似的结果,您可以使用postMessage(向自己的窗口发送消息)。如果您的更新与UI相关,则还可以考虑使用requestAnimationFrame。setTimeout(func,0)根本不像process.nextTick那样工作。 - fabspro
52
“因为他们不喜欢被微软打败”这句话听起来让人感到不悦。主要原因在于它的命名非常糟糕。如果有一个函数永远不会被执行,那就是setImmediate函数立即执行的时候。该函数的名称恰好与其功能完全相反。将nextTick和setImmediate交换可能更好;setImmediate在当前堆栈完成后立即执行(在等待I / O之前),而nextTick则在下一个tick结束时执行(在等待I / O之后)。但是,这已经被说过一千次了。 - Craig Andrews
4
不幸的是,该函数被称为nextTick。 nextTick会立即执行,而setImmediate更像是setTimeout / postMessage。 - Robert
1
@CraigAndrews 我会避免使用requestAnimationFrame,因为它并不总是发生(我肯定见过这种情况,例如当前标签页不是该页面),而且它可能会在页面完成绘制之前被调用(即浏览器仍然在忙于绘制中)。 - robocat
显示剩余11条评论

108

以一个例子为说明:

import fs from 'fs';
import http from 'http';
    
const options = {
  host: 'www.stackoverflow.com',
  port: 80,
  path: '/index.html'
};

describe('deferredExecution', () => {
  it('deferredExecution', (done) => {
    console.log('Start');
    setTimeout(() => console.log('setTimeout 1'), 0);
    setImmediate(() => console.log('setImmediate 1'));
    process.nextTick(() => console.log('nextTick 1'));
    setImmediate(() => console.log('setImmediate 2'));
    process.nextTick(() => console.log('nextTick 2'));
    http.get(options, () => console.log('network IO'));
    fs.readdir(process.cwd(), () => console.log('file system IO 1'));
    setImmediate(() => console.log('setImmediate 3'));
    process.nextTick(() => console.log('nextTick 3'));
    setImmediate(() => console.log('setImmediate 4'));
    fs.readdir(process.cwd(), () => console.log('file system IO 2'));
    console.log('End');
    setTimeout(done, 1500);
  });
});

将给出以下输出

Start // synchronous
End // synchronous
nextTick 1 // microtask
nextTick 2 // microtask
nextTick 3 // microtask
setTimeout 1 // macrotask
file system IO 1 // macrotask
file system IO 2 // macrotask
setImmediate 1 // macrotask
setImmediate 2 // macrotask
setImmediate 3 // macrotask
setImmediate 4 // macrotask
network IO // macrotask

我希望这能帮助理解它们之间的差异。

更新:

使用 process.nextTick() 延迟的回调在触发任何其他 I/O 事件之前运行,而使用 setImmediate()的执行会排队等待已经在队列中的任何 I/O 事件。

Node.js 设计模式,作者 Mario Casciaro(可能是有关 node.js/js 的最佳书籍)


5
我认为需要指出的是,当setTimeout()和setImmediate()不在I/O循环中时,它们的执行顺序是非确定性的,取决于进程的性能。例如,如果我们运行以下脚本,它不在I/O循环中(即主模块),则两个计时器执行顺序是不确定的,因为它受到进程性能的限制。参考链接:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ - Actung
正如@Actung所指出的那样,了解setTimetout和setImmediate是否在I/O循环内部非常重要,以确定结果。 - Rajika Imal
为什么I01在最后面? - NSjonas

76

我认为我可以很好地阐述这一点。由于nextTick在当前操作的末尾被调用,递归调用可能会阻止事件循环继续运行。setImmediate通过在事件循环的检查阶段触发来解决这个问题,允许事件循环正常继续。

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

源代码:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

请注意,检查阶段紧随轮询阶段之后。这是因为轮询阶段和I/O回调是您调用setImmediate的最有可能运行的地方。因此,理想情况下,大多数这些调用实际上会相当立即,只是不像nextTick那样立即,它在每个操作之后都会被检查,并且在事件循环之外存在。

让我们来看一个小例子,介绍setImmediateprocess.nextTick之间的区别:

function step(iteration) {
  if (iteration === 10) return;
  setImmediate(() => {
    console.log(`setImmediate iteration: ${iteration}`);
    step(iteration + 1); // Recursive call from setImmediate handler.
  });
  process.nextTick(() => {
    console.log(`nextTick iteration: ${iteration}`);
  });
}
step(0);

假设我们刚刚运行了此程序,并正在执行事件循环的第一次迭代。它将使用迭代零调用step函数,然后注册两个处理程序,一个是setImmediate的,另一个是process.nextTick的。然后我们从setImmediate处理程序中递归调用此函数,该处理程序将在下一个check阶段运行。nextTick处理程序将在当前操作结束时运行,中断事件循环,因此即使它是第二个注册的,它实际上将首先运行。
顺序最终变为:nextTick作为当前操作结束时触发,下一个事件循环开始,正常的事件循环阶段执行,setImmediate触发并递归调用我们的step函数以重新开始整个过程。当前操作结束,nextTick触发等等。
上述代码的输出将是:
nextTick iteration: 0
setImmediate iteration: 0
nextTick iteration: 1
setImmediate iteration: 1
nextTick iteration: 2
setImmediate iteration: 2
nextTick iteration: 3
setImmediate iteration: 3
nextTick iteration: 4
setImmediate iteration: 4
nextTick iteration: 5
setImmediate iteration: 5
nextTick iteration: 6
setImmediate iteration: 6
nextTick iteration: 7
setImmediate iteration: 7
nextTick iteration: 8
setImmediate iteration: 8
nextTick iteration: 9
setImmediate iteration: 9

现在让我们将递归调用从step移动到nextTick处理程序中,而不是setImmediate
function step(iteration) {
  if (iteration === 10) return;
  setImmediate(() => {
    console.log(`setImmediate iteration: ${iteration}`);
  });
  process.nextTick(() => {
    console.log(`nextTick iteration: ${iteration}`);
    step(iteration + 1); // Recursive call from nextTick handler.
  });
}
step(0);

现在我们已经将递归调用step移动到nextTick处理程序中,事情的行为将会有所不同。我们的事件循环的第一次迭代运行并调用step,注册了一个setImmediate处理程序以及一个nextTick处理程序。在当前操作结束后,我们的nextTick处理程序触发,递归调用step并注册另一个setImmediate处理程序以及另一个nextTick处理程序。由于nextTick处理程序在当前操作之后触发,在nextTick处理程序中注册一个nextTick处理程序将导致第二个处理程序在当前处理程序操作完成后立即运行。nextTick处理程序将继续触发,阻止当前事件循环继续进行。在看到单个setImmediate处理程序触发之前,我们将通过所有的nextTick处理程序。
上述代码的输出结果如下:
nextTick iteration: 0
nextTick iteration: 1
nextTick iteration: 2
nextTick iteration: 3
nextTick iteration: 4
nextTick iteration: 5
nextTick iteration: 6
nextTick iteration: 7
nextTick iteration: 8
nextTick iteration: 9
setImmediate iteration: 0
setImmediate iteration: 1
setImmediate iteration: 2
setImmediate iteration: 3
setImmediate iteration: 4
setImmediate iteration: 5
setImmediate iteration: 6
setImmediate iteration: 7
setImmediate iteration: 8
setImmediate iteration: 9

请注意,如果我们没有在递归调用之后中断它并在10次迭代后中止它,那么nextTick调用将继续递归,从而永远不会让事件循环继续到下一阶段。这就是为什么递归使用nextTick时可能会变成阻塞状态,而setImmediate将在下一个事件循环中触发,并且从其中设置另一个setImmediate处理程序不会中断当前事件循环,允许其像正常情况下执行事件循环的阶段。
附注:我同意其他评论者的看法,两个函数的名称可以轻松交换,因为nextTick听起来像它要在下一个事件循环中触发,而当前循环的结尾比下一个循环的开始更“即时”。噢,这就是API成熟并且人们依赖现有接口所得到的结果。

1
重申Node关于使用process.nextTick的警告非常重要。如果你在nextTickQueue中排队了大量的回调函数,你可能会通过确保不到达轮询阶段而使事件循环饥饿。这就是为什么通常应该优先考虑setImmediate的原因。 - jinglebird

32

在回答的评论中,没有明确说明nextTick从宏观语义移到微观语义。

在node 0.9之前(引入setImmediate之前),nextTick在下一个调用栈的开头操作。

自node 0.9以来,nextTick在现有调用栈的末尾操作,而setImmediate在下一个调用栈的开头。

请查看https://github.com/YuzuJS/setImmediate获取工具和详细信息。


16

这里有一些很好的答案,详细解释了它们的工作原理。

只是添加一个回答特定问题的:

我应该何时使用nextTick,何时应该使用setImmediate


始终使用setImmediate


Node.js事件循环、计时器和process.nextTick()文档包括以下内容:

我们建议开发人员在所有情况下都使用setImmediate(),因为它更容易理解(并且它产生的代码与更广泛的环境兼容,如浏览器JS)。


在文档的早期警告中,它警告说process.nextTick可能会导致...

一些糟糕情况,因为它允许您通过递归process.nextTick()调用来“耗尽”您的I/O,从而防止事件循环达到poll阶段。

事实证明,process.nextTick甚至可以耗尽Promises:

Promise.resolve().then(() => { console.log('this happens LAST'); });

process.nextTick(() => {
  console.log('all of these...');
  process.nextTick(() => {
    console.log('...happen before...');
    process.nextTick(() => {
      console.log('...the Promise ever...');
      process.nextTick(() => {
        console.log('...has a chance to resolve');
      })
    })
  })
})

另一方面,setImmediate 更容易被理解,并避免了这些类型的问题:

Promise.resolve().then(() => { console.log('this happens FIRST'); });

setImmediate(() => {
  console.log('this happens LAST');
})
因此,除非有特定需要使用process.nextTick的独特行为,否则建议在所有情况下都使用setImmediate()

11

4

我建议您查看专门介绍Loop的文档部分以获得更好的理解。这里摘录了一些段落:

我们有两个调用对于用户来说非常相似,但它们的名称令人困惑。

  • process.nextTick() 立即在同一个阶段上触发

  • setImmediate() 在下一次迭代或事件循环中触发

实际上,这些名称应该交换。process.nextTick() 比 setImmediate() 更快触发,但这是过去的遗留问题,不太可能改变。


2

你不应该使用process.nextTick()来中断 CPU 密集型操作

你不应该使用 process.nextTick() 来中断这样的操作。这样做会导致一个永远不会清空的微任务队列,你的应用程序将永远被困在同一个阶段中! -- Thomas Hunter. Distributed Systems with Node.js

事件循环的每个阶段都包含多个回调函数:

One phase of loop event

一旦 process.nextTick() 被触发,它将始终保持在相同的阶段。

示例

const nt_recursive = () => process.nextTick(nt_recursive);
nt_recursive(); // setInterval will never run

const si_recursive = () => setImmediate(si_recursive);
si_recursive(); // setInterval will run

setInterval(() => console.log('hi'), 10);

在这个例子中,setInterval() 表示应用程序执行的一些异步工作,例如响应传入的 HTTP 请求。

一旦运行 nt_recursive() 函数,应用程序就会得到一个永远不会清空的微任务队列,异步工作永远不会得到处理。

但是替代版本 si_recursive() 没有相同的副作用。

在检查阶段内使用 setImmediate() 调用会将回调添加到下一个事件循环迭代的检查阶段队列中,而不是当前阶段的队列中。

情况可能使用 process.nextTick

使函数全部异步化。

function foo(count, callback) {
  if (count <= 0) {
    return process.nextTick(() => callback(new TypeError('count > 0')));
  }
  myAsyncOperation(count, callback);
}

在这种情况下,使用setImmediate()process.nextTick()都可以,只要确保不会意外引入递归。——Thomas Hunter.使用Node.js进行分布式系统

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接