【问题标题】:setImmediate vs. nextTicksetImmediate vs. nextTick
【发布时间】:2023-03-19 12:00:01
【问题描述】:

Node.js 0.10 版今天发布,引入了setImmediateAPI changes 文档建议在进行递归 nextTick 调用时使用它。

MDN says 看来,它与process.nextTick 非常相似。

什么时候应该使用nextTick,什么时候应该使用setImmediate

【问题讨论】:

  • 博客blog.nodejs.org/2013/03/11/node-v0-10-0-stable有5段关于此更改的内容
  • 从性能基准来看,nextTick 在大型计算中比setImmediate 更快。
  • 为了记录,我首先阅读了这五段,但当它并没有真正为我弄清楚任何事情时,我仍然最终解决了这个问题。接受的答案更简洁,实际上更详细地描述了setImmediate 所做的事情。
  • 我已经在我的blog 中详细解释了差异。
  • 是不是GC可以在setImmediate之前运行,但不能在nextTick之前运行?

标签: javascript node.js setimmediate


【解决方案1】:

作为说明:

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 的最佳书籍)

【讨论】:

  • 这真的很有帮助,谢谢。我认为图像和示例是理解事物的最快方式。
  • 我认为重要的是要指出 setTimeout() 和 setImmediate() 不在 I/O 周期内时,顺序是不确定的,具体取决于进程的性能。 nodejs.org/en/docs/guides/event-loop-timers-and-nexttick For example, if we run the following script which is not within an I/O cycle (i.e. the main module), the order in which the two timers are executed is non-deterministic, as it is bound by the performance of the process: 所以,这个答案并没有真正回答确切的区别,而只是一个在不同情况下可能会有所不同的例子
  • 正如@Actung 所指出的。了解 setTimetout 和 setImmediate 是否在一个 I/O 周期内对于确定结果非常重要。
  • 为什么I01在最后?
【解决方案2】:

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

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

事件循环的每个阶段都包含几个回调:

一旦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);
}

-- Thomas Hunter. Distributed Systems with Node.js

在这种情况下,使用setImmediate()process.nextTick() 都可以;只要确保你不会意外引入递归。

【讨论】:

    【解决方案3】:

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

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

    什么时候应该使用nextTick,什么时候应该使用setImmediate


    始终使用setImmediate


    Node.js Event Loop, Timers, and 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()”。

    【讨论】:

      【解决方案4】:

      我建议您查看docs Loop 专用部分以获得更好的理解。从那里获取的一些sn-p:

      就用户而言,我们有两个类似的调用,但它们的名称令人困惑。

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

      • setImmediate() 在下一次迭代或
        的“滴答”时触发 事件循环

      本质上,名称应该互换。 process.nextTick() 比 setImmediate() 更立即触发,但这是过去的产物,不太可能改变。

      【讨论】:

        【解决方案5】:

        简单来说,process.NextTick() 将在事件循环的下一个滴答声中执行。但是,setImmediate 基本上有一个单独的阶段,它确保在 setImmediate() 下注册的回调只有在 IO 回调和轮询阶段之后才会被调用。

        请参考此链接以获得很好的解释: https://medium.com/the-node-js-collection/what-you-should-know-to-really-understand-the-node-js-event-loop-and-its-metrics-c4907b19da4c

        【讨论】:

          【解决方案6】:

          我想我可以很好地说明这一点。由于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 处理程序中递归调用此函数,该处理程序将在下一个检查阶段运行。 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 注册setImmedaite 处理程序以及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 处理程序根本不会中断当前事件循环,从而允许它继续执行阶段事件循环正常。

          希望有帮助!

          PS - 我同意其他评论者的观点,即这两个函数的名称可以很容易地互换,因为 nextTick 听起来它会在下一个事件循环中触发,而不是在当前事件循环的结尾,以及当前循环比下一个循环的开始更“立即”。哦,好吧,随着 API 的成熟和人们开始依赖现有的接口,这就是我们所得到的。

          【讨论】:

          • 很清楚的描述。我认为这个答案需要更多的支持。
          • 很好解释(Y)
          • 重申 Node 关于使用 process.nextTick 的警告很重要。如果您在 nextTickQueue 中加入大量回调,您可能会通过确保永远不会到达轮询阶段来使事件循环饿死。这就是为什么您通常应该更喜欢 setImmediate 的原因。
          • 谢谢,这是最好的解释。示例代码真的很有帮助。
          • @skyhavoc 很高兴我能帮上忙!
          【解决方案7】:

          在答案中的 cmets 中,它没有明确说明 nextTick 从宏语义转移到微语义。

          在节点 0.9 之前(引入 setImmediate 时),nextTick 在下一个调用堆栈的开始处操作。

          从节点 0.9 开始,nextTick 在现有调用栈的末尾运行,而 setImmediate 在下一个调用栈的开头

          查看https://github.com/YuzuJS/setImmediate 了解工具和详细信息

          【讨论】:

            【解决方案8】:

            如果您想在事件队列中已经存在的任何 I/O 事件回调之后对函数进行排队,请使用 setImmediate。使用process.nextTick 将函数有效地排在事件队列的头部,以便在当前函数完成后立即执行。

            因此,如果您尝试使用递归分解长期运行的、受 CPU 限制的作业,您现在需要使用 setImmediate 而不是 process.nextTick 来排队下一次迭代,否则任何 I/ O 事件回调将没有机会在迭代之间运行。

            【讨论】:

            • 传递给 process.nextTick 的回调通常会在当前执行流程结束时被调用,因此与同步调用函数的速度差不多。如果不加以检查,这会使事件循环饿死,从而阻止任何 I/O 发生。 setImmediates 按创建的顺序排队,并在每次循环迭代时从队列中弹出一次。这与 process.nextTick 不同,它将在每次迭代时执行 process.maxTickDepth 排队回调。 setImmediate 将在触发队列回调以确保 I/O 没有被饿死后屈服于事件循环。
            • @UstamanSangat setImmediate 仅受 IE10+ 支持,所有其他浏览器都固执地拒绝实施可能的未来标准,因为它们不喜欢被微软击败。要在 FF/Chrome 中实现类似的结果,您可以使用 postMessage(将消息发布到您自己的窗口)。您也可以考虑使用 requestAnimationFrame,尤其是当您的更新与 UI 相关时。 setTimeout(func, 0) 根本不像 process.nextTick 那样工作。
            • @fabspro “因为他们不喜欢被我的微软打败”让你听起来对某事感到痛苦。这主要是因为它的名字非常可怕。如果有一次 setImmediate 函数永远不会运行,它会立即运行。该函数的名称与它的作用完全相反。 nextTick 和 setImmediate 最好不要切换; setImmediate 在当前堆栈完成后立即执行(在等待 I/O 之前),并且 nextTick 在下一个滴答结束时执行(在等待 I/O 之后)。但是,这已经说了一千遍了。
            • @fabspro 但不幸的是,该函数被称为 nextTick。 nextTick “立即”执行,而 setImmediate 更像是 setTimeout/postMessage。
            • @CraigAndrews 我会避免requestAnimationFrame,因为它并不总是发生(我肯定看到过这个,我认为示例是标签不是当前标签)并且它可以在页面之前被调用完成绘画(即浏览器仍在忙着绘画)。
            猜你喜欢
            • 2015-02-23
            • 2014-04-26
            • 2013-07-04
            • 2020-10-06
            • 2021-10-26
            • 2014-07-29
            • 2017-06-04
            相关资源
            最近更新 更多