GVKun编程网logo

了解node.js事件队列和process.nextTick(nodejs事件机制)

8

对于想了解了解node.js事件队列和process.nextTick的读者,本文将提供新的信息,我们将详细介绍nodejs事件机制,并且为您提供关于javascript–Node.js–诊断“(节点

对于想了解了解node.js事件队列和process.nextTick的读者,本文将提供新的信息,我们将详细介绍nodejs事件机制,并且为您提供关于javascript – Node.js – 诊断“(节点)警告:检测到递归process.nextTick.”、javascript – 理解node.js事件队列和process.nextTick、Node.js Event Loop 的理解 Timers,process.nextTick()、Node.js Event Loop之Timers, process.nextTick()的有价值信息。

本文目录一览:

了解node.js事件队列和process.nextTick(nodejs事件机制)

了解node.js事件队列和process.nextTick(nodejs事件机制)

我无法确切了解process.nextTick其功能。我以为自己了解了,但似乎无法复制自己的感觉:

var handler = function(req, res) {    res.writeHead(200, {''Content-type'' : ''text/html''});    foo(function() {        console.log("bar");    });    console.log("received");    res.end("Hello, world!");}function foo(callback) {    var i = 0;    while(i<1000000000) i++;    process.nextTick(callback);}require(''http'').createServer(handler).listen(3000);

虽然foo是循环的,我会寄过来几个请求,假设handler将排队几次身后foocallback只在被排队foo完成。

如果我对它的工作方式是正确的,我认为结果将如下所示:

receivedreceivedreceivedreceivedbarbarbarbar

但这不是连续的:

receivedbarreceivedbarreceivedbarreceivedbar

我看到这foo是在执行之前返回的,callback这是预期的,但似乎callback是NEXT在行,而不是在队列的末尾,在所有请求的后面。这是它的工作方式吗?也许我只是不了解节点中事件队列的工作原理。而且请不要在这里指点我。谢谢。

答案1

小编典典

process.nextTick将该回调放在将要执行的下一个刻度上,而不是在刻度队列的末尾。

Node.js文档(http://nodejs.org/api/process.html#process_process_nexttick_callback)说:“它通常在任何其他I
/ O事件触发之前运行,但是有一些例外。”

setTimeout(callback,0)可能会像您描述的那样工作。

javascript – Node.js – 诊断“(节点)警告:检测到递归process.nextTick.”

javascript – Node.js – 诊断“(节点)警告:检测到递归process.nextTick.”

我的应用程序有一种情况,即操作需要很长时间才能完成,然后打印以下消息的几百个实例:

(node) warning: Recursive process.nextTick detected. This will break in the next version of node. Please use setImmediate for recursive deferral.

然后应用程序崩溃:

RangeError: Maximum call stack size exceeded

没有堆栈跟踪.

我如何诊断此类错误?我想知道,例如,哪个函数调用在RangeError之前.

要复制错误,请在文件foo.js上运行节点foo.js,其中包含以下内容:

var foo = function () {
    process.nextTick(foo);
};
foo();

我对应用程序中此问题的特定原因并不感兴趣,因为我知道如何在节点中诊断此类问题.

节点版本为v0.10.39.

解决方法

您可以替换process.nextTick来打印某种有用的信息 – 这些信息的行列如下:

var _nextTick = process.nextTick;
var alreadyLogged = new Set();

process.nextTick = function nextTick(f) {
    if (!alreadyLogged.has(f)) {
        alreadyLogged.add(f);

        process.nextTick = _nextTick;
        console.log(f);
        process.nextTick = nextTick;
    }

    return _nextTick.apply(this,arguments);
};

javascript – 理解node.js事件队列和process.nextTick

javascript – 理解node.js事件队列和process.nextTick

我无法准确理解process.nextTick如何做到这一点.我以为我理解了,但我似乎无法复制我觉得这应该如何工作:
var handler = function(req,res) {
    res.writeHead(200,{'Content-type' : 'text/html'});
    foo(function() {
        console.log("bar");
    });
    console.log("received");
    res.end("Hello,world!");
}

function foo(callback) {
    var i = 0;
    while(i<1000000000) i++;
    process.nextTick(callback);
}

require('http').createServer(handler).listen(3000);

当foo循环时,我会发送几个请求,假设处理程序将在foo后面排队几次,只有当foo完成后才会将回调排入队列.

如果我对这是如何工作正确的,我认为结果将如下所示:

received
received
received
received
bar
bar
bar
bar

但它没有,它只是顺序的:

received
bar
received
bar
received
bar
received
bar

我看到foo在执行回调之前正在返回,这是预期的,但似乎回调是NEXT在行,而不是在队列的末尾,在所有请求进入后面.这是否有效?也许我只是不明白节点中的事件队列是如何工作的.请不要指向我here.谢谢.

解决方法

process.nextTick将回调放在将要执行的下一个tick上,而不是在tick队列的末尾.

Node.js doc(http://nodejs.org/api/process.html#process_process_nexttick_callback)说:
“它通常在任何其他I / O事件发生之前运行,但也有一些例外情况.”

setTimeout(callback,0)可能更像你描述的那样工作.

Node.js Event Loop 的理解 Timers,process.nextTick()

Node.js Event Loop 的理解 Timers,process.nextTick()

写这篇文章的目的是将自己对该文章的理解做一个记录,官方文档链接The Node.js Event Loop, Timers, and process.nextTick()

文章内容可能有错误理解的地方,希望能和大家探讨一下,欢迎批评指正!

Node.js Event Loop 的理解 Timers,process.nextTick()

Event Loop的解释

英文原文: When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,

   ┌───────────────────────┐
┌─>│ timers   └──────────┬────────────┘  ┌──────────┴────────────┐   I/O callbacks   └──────────┬────────────┘  ┌──────────┴────────────┐   idle, prepare   └──────────┬────────────┘ ┌───────────────┐  ┌──────────┴────────────┐  incoming:    poll │<─────┤ connections,   └──────────┬────────────┘  data, etc.   ┌──────────┴────────────┐ └───────────────┘   check   └──────────┬────────────┘  ┌──────────┴────────────┐ └──┤ close callbacks  └───────────────────────┘
  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时, node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时, event loop会转入下一下阶段.

注意上面六个阶段都不包括 process.nextTick()

poll阶段

poll阶段是衔接整个event loop各个阶段比较重要的阶段,为了便于后续例子的理解,本文和原文的介绍顺序不一样,本文先讲这个阶段;

在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。

poll 阶段有两个主要的功能:

  1. 处理poll队列(poll quenue)的事件(callback);
  2. 执行timers的callback,当到达timers指定的时间时;

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
  • 如果poll queue为空,将会发生下面情况:
    • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;

如果event loop进入了 poll阶段,且代码设定了timer:

  • 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.

以上便是整个event loop时间循环的各个阶段运行机制,有了这层理解,我们来看几个例子

  • 注意,例子中给出的时间在不同机器下和同一机器下不同执行时刻,其值都会有差异。

example 1

var fs = require(''fs''); function someAsyncOperation (callback) { // 花费2毫秒 fs.readFile(__dirname + ''/'' + __filename, callback); } var timeoutScheduled = Date.now(); var fileReadTime = 0; setTimeout(function () { var delay = Date.now() - timeoutScheduled; console.log(''setTimeout: '' + (delay) + "ms have passed since I was scheduled"); console.log(''fileReaderTime'',fileReadtime - timeoutScheduled); }, 10); someAsyncOperation(function () { fileReadtime = Date.now(); while(Date.now() - fileReadtime < 20) { } });

结果: 先执行someAsyncOperation的callback,再执行setTimeout callback

-> node eventloop.js
setTimeout: 22ms have passed since I was scheduled fileReaderTime 2

解释: 当时程序启动时,event loop初始化:

  1. timer阶段(无callback到达,setTimeout需要10毫秒)
  2. i/o callback阶段,无异步i/o完成
  3. 忽略
  4. poll阶段,阻塞在这里,当运行2ms时,fs.readFile完成,将其callback加入 poll队列,并执行callback, 其中callback要消耗20毫秒,等callback之行完,poll处于空闲状态,由于之前设定了timer,因此检查timers,发现timer设定时间是20ms,当前时间运行超过了该值,因此,立即循环回到timer阶段执行其callback,因此,虽然setTimeout的20毫秒,但实际是22毫秒后执行。

example 2

var fs = require(''fs''); function someAsyncOperation (callback) { var time = Date.now(); // 花费9毫秒 fs.readFile(''/path/to/xxxx.pdf'', callback); } var timeoutScheduled = Date.now(); var fileReadTime = 0; var delay = 0; setTimeout(function () { delay = Date.now() - timeoutScheduled; }, 5); someAsyncOperation(function () { fileReadtime = Date.now(); while(Date.now() - fileReadtime < 20) { } console.log(''setTimeout: '' + (delay) + "ms have passed since I was scheduled"); console.log(''fileReaderTime'',fileReadtime - timeoutScheduled); });

结果:setTimeout callback先执行,someAsyncOperation callback后执行

-> node eventloop.js
setTimeout: 7ms have passed since I was scheduled fileReaderTime 9

解释: 当时程序启动时,event loop初始化:

  1. timer阶段(无callback到达,setTimeout需要10毫秒)
  2. i/o callback阶段,无异步i/o完成
  3. 忽略
  4. poll阶段,阻塞在这里,当运行5ms时,poll依然空闲,但已设定timer,且时间已到达,因此,event loop需要循环到timer阶段,执行setTimeout callback,由于从poll --> timer中间要经历check,close阶段,这些阶段也会消耗一定时间,因此执行setTimeout callback实际是7毫秒 然后又回到poll阶段等待异步i/o完成,在9毫秒时fs.readFile完成,其callback加入poll queue并执行。

setTimeout 和 setImmediate

二者非常相似,但是二者区别取决于他们什么时候被调用.

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

setTimeout(function timeout () { console.log(''timeout''); },0); setImmediate(function immediate () { console.log(''immediate''); });
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js immediate timeout

关于这点的原因,笔者目前还未弄清楚。。。其原因读者可去看@hyj1991 的留言,讲解非常清晰

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

var fs = require(''fs'') fs.readFile(__filename, () => { setTimeout(() => { console.log(''timeout'') }, 0) setImmediate(() => { console.log(''immediate'') }) })
$ node timeout_vs_immediate.js
immediate
timeout

理解了event loop的各阶段顺序这个例子很好理解: 因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout

process.nextTick()

千呼万唤始出来,终于到了讲process.nextTick()的时候,来来来喝口水休息休息。

注意:本文讲解的process.nextTick是基于v0.10及以上版本

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。

var fs = require(''fs''); fs.readFile(__filename, () => { setTimeout(() => { console.log(''setTimeout''); }, 0); setImmediate(() => { console.log(''setImmediate''); process.nextTick(()=>{ console.log(''nextTick3''); }) }); process.nextTick(()=>{ console.log(''nextTick1''); }) process.nextTick(()=>{ console.log(''nextTick2''); }) });
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout

从poll —> check阶段,先执行process.nextTick, nextTick1 nextTick2 然后进入check,setImmediate, setImmediate 执行完setImmediate后,出check,进入close callback前,执行process.nextTick nextTick3 最后进入timer执行setTimeout setTimeout

process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。

example process.nextTick()

当递归调用process.nextTick时,即使fs.readFile完成,其callback无机会执行

var fs = require(''fs''); var starttime = Date.now(); var endtime = 0; fs.readFile(__filename, () => { endtime = Date.now(); console.log(''finish reading time:'',endtime - starttime); }); var index = 0; function nextTick () { if (index > 1000) return; index++; console.log(''nextTick''); process.nextTick(nextTick); } nextTick();
-> node eventloop.js
nextTick
nextTick
... nextTick nextTick finish reading time: 246

example setImmediate

将process.nextTick替换成setImmediate后,由于setImmediate只在check阶段执行,那么所有的callback都有机会执行。

var fs = require(''fs''); fs.readFile(__filename, () => { console.log(''finish reading''); }); var index = 0; function Immediate () { if (index > 100) return; index++; console.log(''setImmediate''); setImmediate(Immediate); } Immediate()
-> node eventloop.js
setImmediate
setImmediate
setImmediate
setImmediate
finish reading time: 19 ... setImmediate setImmediate

Node.js Event Loop之Timers, process.nextTick()

Node.js Event Loop之Timers, process.nextTick()

前言

Node.js以异步I/O和事件驱动的特性著称,但异步I/O是怎么实现的呢?其中核心的一部分就是event loop,下文中内容基本来自于Node.js文档,有不准确地方请指出.

什么是Event loop

event loop能让Node.js的I/O操作表现得无阻塞,尽管JavaScript是单线程的但通过尽可能的将操作放到操作系统内核.

由于现在大多数内核都是多线程的,它们可以在后台执行多个操作. 当这些操作完成时,内核通知Node.js应该把回调函数添加到poll队列被执行.我们将在接下来的话题里详细讨论.

Event Loop 说明

当Node.js开始时,它将会初始化event loop,处理提供可能造成异步API调用,timers任务,或调用process.nextTick()的脚本(或者将它放到[REPL][]中,这篇文章中将不会讨论),然后开始处理event loop.

下面是一张event loop操作的简单概览图.

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

注意: 每一个方框将被简称为一个event loop的阶段.

每一个阶段都有一个回调函数的FIFO队列被执行.每一个阶段都有自己特有的方式,通常even loop进入一个给定的阶段时,它将执行该阶段任何的特定操作,然后执行该阶段队列中的回调函数,直到执行完所有回调或执行了最大回调的次数.当队列中的回调已被执行完或者到达了限制次数,eventloop将会从下一个阶段开始依次执行.

由于这些操作可能造成更多的操作,并且在poll阶段中产生的新事件被内核推入队列,所以poll事件可以被推入队列当有其它poll事件正在执行时.因此长时间执行回调可以允许poll阶段超过timers设定的时间.详细内容请看timerspoll章节.

ps: 个人理解-在轮询阶段一个回调执行可能会产生新的事件处理,这些新事件会被推入到轮询队列中,所以poll阶段可以一直执行回调,即使timers的回调已到时间应该被执行时.

注意: Windows和Unix/Linux在实现时有一些细微的差异,但那都不是事儿.重点是: 实际上有7或8个步骤,Node.js实际上使用的是它们所有.

阶段概览

  • timers: 这个阶段执行setTimeout()setInterval()产生的回调.
  • I/O callbacks: 执行大多数的回调,除了close callbacks,timers和setImmediate()的回调.
  • idle,prepare: 仅供内部使用.
  • poll: 获取新的I/O事件;node会在适当时候在这里阻塞.
  • check: 执行setImmediate()回调.
  • close callbacks: e.g. socket.on('close',...).

在每次event loop之间,Node.js会检查它是否正在等待任何异步I/O或计时器,如果没有就会完全关闭.

阶段详情

timers

一个定时器指定的是执行回调函数的阈值,而不是确定的时间点.定时器的回调将在规定的时间过后运行;然而,操作系统调度或其他回调函数的运行可能会使执行回调延迟.

注意: 技术上,poll 阶段控制了timers被执行.

例如,你要在100ms的延时后在回调函数并且执行一个耗时95ms的异步读取文本操作:

const fs = require('fs');

function someAsyncoperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file',callback);
}

const timeoutScheduled = Date.Now();

setTimeout(function() {

  const delay = Date.Now() - timeoutScheduled;

  console.log(delay + 'ms have passed since I was scheduled');
},100);


// do someAsyncoperation which takes 95 ms to complete
someAsyncoperation(function() {

  const startCallback = Date.Now();

  // do something that will take 10ms...
  while (Date.Now() - startCallback < 10) {
    // do nothing
  }

});

// 输出: 105ms have passed since I was scheduled

当event loop进入poll阶段时,它是一个空的队列(fs.readFile()还没有完成),所以它会等待数毫秒等待timers设定时间的到达.直到等待95 ms过后,fs.readFile()完成文件读取然后它的回调函数会被添加至poll队列然后执行.当执行完成后队列中没有其他回调,所以event loop会查看定时器设定的时间已经到达然后回撤到timers阶段执行timers的回调函数.在例子里你会发现,从定时器被记录到执行回调函数耗时105ms.

注意: 为了防止poll阶段阻塞死event loop,[libuv]
(http://libuv.org/) (实现Node.js事件循环的C库和平台的所有异步行为)
也有一个固定最大值(系统依赖).

I/O callbacks

这个阶段执行一些系统操作的回调,例如TCP错误等类型.例如TCP socket 尝试连接时收到了ECONNREFUSED,一些*nix系统想等待错误日志记录.这些都将在I/O callbacks阶段被推入队列执行.

poll

poll 阶段有两个主要的功能:

  1. 为已经到达或超时的定时器执行脚本
  2. 处理在poll队列中的事件.

当event loop进入poll阶段并且没有timers任务时会执行下面某一条操作:

  • 如果poll队列不为空,则event loop会同步的执行回调队列,直到执行完回调或达到系统最大限制.
  • 如果poll队列为空,会执行下面某一条操做:

    • 如果脚本被setImmediate()执行,则event loop会结束 poll阶段,继续向下进入到check阶段执行setImmediate()的脚本.
    • 如果脚本不是被setImmediate()执行,event loop会等待回调函数被添加至队列,然后立刻执行它们.

一旦poll队列空了,event loop会检查timers是否有以满足条件的定时器,如果有一个以上满足执行条件的定时器,event loop将会撤回至timers阶段去执行定时器的回调函数.

check

这个阶段允许立刻执行一个回调在poll阶段完成后.如果poll阶段已经执行完成或脚本已经使用setImmediate(),event loop 可能就会继续到check阶段而不是等待.

setImmediate()实际是在event loop 独立阶段运行的特殊定时器.它使用了libuv API来使回调函数在poll阶段后执行.

通常在代码执行时,event loop 最终会到达poll阶段,等待传入连接,请求等等.然而,如果有一个被setImmediate()执行的回调,poll阶段会变得空闲,它将会结束并进入check阶段而不是等待新的poll事件.

close callbacks

如果一个socket或者操作被突然关闭(例如.socket.destroy()),这个close事件将在这个阶段被触发.否则它将会通过process.nextTick()被触发.

setImmediate() vs setTimeout()

setImmediatesetTimeout() 是很相似的,但是它们的调用方式不同导致了会有不同的表现.

  • setImmediate() 会中断poll阶段,立即执行..
  • setTimeout() 将在给定的毫秒后执行设定的脚本.

timers的执行顺序会根据它们被调用的上下文而变化.如果两个都在主模块内被调用,则时序将受到进程的性能的限制(可能受机器上运行的其他应用程序的影响).

例如,我们执行下面两个不在I/O周期内(主模块)的脚本,这两个timers的执行顺序是不确定的,它受到进程性能的影响:

// timeout_vs_immediate.js
setTimeout(function timeout() {
  console.log('timeout');
},0);

setImmediate(function immediate() {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

然而,如果你把这两个调用放到I/O周期内,则immediate的回调总会被先执行:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename,() => {
  setTimeout(() => {
    console.log('timeout');
  },0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

使用setImmediate()setTimeout()的好处是setImmediate()在I/O周期内总是比所有timers先执行,无论有多少timers存在.

process.nextTick()

理解 process.nextTick()

你可能已经注意到process.nextTick()没有在概览图中列出,尽管他是异步API的一部分.这是因为process.nextTick()在技术上不是event loop的一部分.反而nextTickQueue会在当前操作完成后会被执行,无论当前处于event loop的什么阶段.

再看看概览图,在给定的阶段你任何时候调用process.nextTick(),通过process.nextTick()指定的回调函数都会在event loop继续执行前被解析.这可能会造成一些不好的情况,因为它允许你通过递归调用process.nextTick()而造成I/O阻塞死,因为它阻止了event loop到达poll阶段.

为什么这种操作会被允许呢?

部分原因是一个API应该是异步事件尽管它可能不是异步的.看看下面代码片段:

function apiCall(arg,callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,new TypeError('argument should be string'));
}

代码里对参数做了校验,如果不正确,它将会在回调函数中抛出错误.API最近更新,允许传递参数给 process.nextTick() ,process.nextTick()可以接受任何参数,回调函数被当做参数传递给回调函数后,你就不必使用嵌套函数了.

我们所做的就是将错误回传给用户当用户的其它代码执行后.通过使用process.nextTick()我们确保apiCall()执行回调函数在用户的代码之后,在event loop运行的阶段之前.为了实现这一点,JS调用的堆栈被允许释放掉,然后立刻执行提供的回调函数,回调允许用户递归的调用process.nextTick()直到v8限制的调用堆栈最大值.

这种理念可能会导致一些潜在的问题.来看这段代码:

let bar;

// this has an asynchronous signature,but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {

  // since someAsyncApiCall has completed,bar hasn't been assigned any value
  console.log('bar',bar); // undefined

});

bar = 1;

用户定义了一个有异步标签的函数someAsyncApiCall(),尽管他的操作是同步的.当它被调用的时候,提供的回调函数在event loop的同一阶段中被调用,因为someAsyncApiCall()没有任何异步操作.所以回调函数尝试引用bar尽管这个变量在作用域没有值,因为代码还没有执行到最后.

通过将回调函数放在process.nextTick()里,代码仍然有执行完的能力,允许所有的变量,函数等先被初始化来供回调函数调用.它还有不允许event loop继续执行的优势.它可能在event loop继续执行前抛出一个错误给用户很有用.这里提供一个使用process.nextTick()的示例:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar',bar); // 1
});

bar = 1;

这里有另一个真实的例子:

const server = net.createServer(() => {}).listen(8080);

server.on('listening',() => {});

仅当端口可用时端口立即被绑定.所以'listening'的回调函数能立即被调用.问题是那时候不会设置.on('listening').

为了解决这个问题,'listening'事件被放入nextTick()队列来允许代码执行完.这会允许用户设置他们想要的任何事件处理.

process.nextTick() vs setImmediate()

我们有两个直到现在用户都关心的相似的调用,但他们的名字令人困惑.

  • process.nextTick() 在同一阶段立即触发
  • setImmediate() 在以下迭代器或者event loop的'tick'中触发

本质上,这两个名字应该交换.process.nextTick()setImmediate()触发要快但这是一个不想改变的历史的命名.做这个改变会破坏npm上大多数包.每天都有新模块被增加,意味着每天我们都在等待更多的潜在错误发生.当他们困惑时,这个名字就不会被改变.

我们建议开发者使用setImmediate()因为它更容易被理解(并且它保持了更好的兼容性,例如浏览器的JS)

为什么使用process.nextTick()?

有两个主要原因:

  1. 允许用户处理错误,清除任何不需要的资源,或者可能在事件循环继续之前再次尝试该请求.
  2. 同时有必要允许回调函数执行在调用堆栈释放之后但在event loop继续之前.

一个满足用户期待的简单例子:

const server = net.createServer();
server.on('connection',function(conn) { });

server.listen(8080);
server.on('listening',function() { });

listen()在event loop开始时执行,但是listening的回调函数被放在一个setImmediate()中.现在除非主机名可用于绑定端口会立即执行.现在为了event loop继续执行,它必须进入poll阶段,意味着在监听事件前且没有触发允许连接事件时没有接收到请求的可能.

另一个例子是运行一个函数构造函数,例如,继承自EventEmitter,并且想要在构造函数中调用一个事件:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event');
}
util.inherits(MyEmitter,EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event',function() {
  console.log('an event occurred!');
});

你不能在构造函数中立即触发事件,因为代码不会执行到用户为该事件分配回调函数的地方,所以,在构造函数本身中,你可以使用process.nextTick()设置回调函数来在够咱函数完成后触发事件.有一个小栗子:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter,function() {
  console.log('an event occurred!');
});

部分个人理解

前面基本是基于文档的翻译(由于英文能力问题,很多地方都模模糊糊,甚至是狗屁不通[捂脸]),下面写一些重点部分的理解

几个概念

  1. event loop是跑在主进程上的一个while(true) {}循环.
  2. timers阶段包括setTimeout(),setInterval()两个定时器,回调执行时间等于或者晚于定时器设定的时间,因为在poll阶段会执行其它回调函数,在空闲时才回去检查定时器(event loop的开始和结束时检查).
  3. 在I/O callback阶段,虽然在阶段介绍里说的是执行除timers,Immediate,close之外的所有回调,但后面详细介绍中又说了,这里执行的大多是stream,pipe,tcp,udp通信错误的回调,例如fs产生的回调应该还是在poll阶段执行的.
  4. poll阶段应该才是真正的执行了除timers,close外的所有回调.
  5. process.nextTick()没有在任何一个阶段执行,它执行的时间应该是在各个阶段切换的中间执行.

几段代码

const fs = require('fs');

fs.readFile('../mine.js',() => {
    setTimeout(() => { console.log("setTimeout") },0);
    process.nextTick(() => { console.log("process.nextTick") })
    setImmediate(() => { console.log("setImmediate") })
});
/*log -------------------
process.nextTick
setImmediate
setTimeout
*/
  1. 当文件读取完成后在poll阶段执行回调函数
  2. setTimeout添加至timers队列,解析process.nextTick()回调函数,将setImmediate添加至check队列
  3. poll队列为空,有setImmediate的代码,继续向下一个阶段.
  4. 在到达check阶段前执行process.nextTick()回调函数
  5. check阶段执行setImmediate
  6. timers阶段执行setTimeout回调
const fs = require('fs');

const start = new Date();
fs.readFile('../mine.js',() => {
    setTimeout(() => { console.log("setTimeout spend: ",new Date() - start) },0);
    setImmediate(() => { console.log("setImmediate spend: ",new Date() - start) })
    process.nextTick(() => { console.log("process.nextTick spend: ",new Date() - start) })
});
setTimeout(() => { console.log("setTimeout-main spend: ",0);
setImmediate(() => { console.log("setImmediate-main spend: ",new Date() - start) })
process.nextTick(() => { console.log("process.nextTick-main spend: ",new Date() - start) })
/* log ----------------
process.nextTick-main spend:  9
setTimeout-main spend:  12
setImmediate-main spend:  13
process.nextTick spend:  14
setImmediate spend:  15
setTimeout spend:  15
*/

这里没有搞懂为什么主进程内的setTimeout总是比setImmediate先执行,按文档所说,两个应该是不确定谁先执行.

@H_301_499@

今天关于了解node.js事件队列和process.nextTicknodejs事件机制的分享就到这里,希望大家有所收获,若想了解更多关于javascript – Node.js – 诊断“(节点)警告:检测到递归process.nextTick.”、javascript – 理解node.js事件队列和process.nextTick、Node.js Event Loop 的理解 Timers,process.nextTick()、Node.js Event Loop之Timers, process.nextTick()等相关知识,可以在本站进行查询。

本文标签: