Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NodeJS 系列之事件循环 #10

Open
Ziphwy opened this issue Nov 11, 2020 · 0 comments
Open

NodeJS 系列之事件循环 #10

Ziphwy opened this issue Nov 11, 2020 · 0 comments

Comments

@Ziphwy
Copy link
Owner

Ziphwy commented Nov 11, 2020

NodeJS 的事件循环(EventLoop)和浏览器是不一样的,NodeJS 使用 libuv 实现事件循环和所有异步行为。

NodeJS EventLoop 的流程

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

查看 libuv 对上述流程的实现

从官方流程图可以发现,NodeJS 的事件循环(EventLoop)是多任务队列,事件循环(EventLoop)将按照上图流程进入逐个阶段。

1 - Timer 阶段

setTimeoutsetInterval 的回调会进入本阶段队列。
事件循环进入本阶段,会检查并执行到时的计时器回调,如果没有,结束此阶段。

2 - Pending callbacks 阶段

该阶段执行上一轮循环被延迟的某些系统操作回调(比如 TCP 错误)。

3 - Idle, prepare 阶段

提供给 NodeJS 内部使用。

4 - Poll 阶段

close 外的所有 I/O 回调会被推进该阶段的队列。

本阶段会计算需要阻塞和等待 I/O 的时间,并按下面步骤处理:

  1. 检查 I/O 回调队列,如果有,同步执行直到清空队列或者到达系统上限。
  2. 检查 setImmediate 回调队列,如果有,进入 Check 阶段。
  3. 检查是否有到时的计时器,如果有,回到 Timer 阶段。
  4. 如果都没有,则会阻塞在此阶段,等待新的异步任务,执行上述步骤处理,直到等待时间结束。

该阶段被称为轮询(Poll)的原因大概是,几乎所有的异步回调都在本阶段处理,并且会阻塞等待新的异步任务,根据新的任务类型发生阶段流转。

5 - Check 阶段

执行 setImmediate 注册的回调。

6 - Close callbacks 阶段

执行 I/O 的 close 回调,如果没有,则结束本阶段。

容易混淆的概念

setImmediate vs setTimeout

案例:

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

// 输出 ?

setImmediate 回调在 Check 阶段执行,setTimeout 回调在 Timer 阶段执行,理论上,setTimeout 回调应该更早执行,实际上上述代码输出是随机的,这与系统和进程性能有关。

因为 setTimeout 指定的时间是有下限的,虽然指定了 0,但是最小只能是 1ms

When delay is larger than 2147483647 or less than 1, the delay will be set to 1. Non-integer delays are truncated to an integer.

from NodeJS Timer 文档

  • 如果性能好执行快,进入 Timer 阶段时还没到 1ms,setTimeout 的回调未到时,最后到 Check 阶段执行 setImmediate 回调,然后再第二次循环才能执行;
  • 如果性能慢,进入 Timer 阶段时到了 1ms,setTimeout 的回调会先执行。

如果把它们放入一个 I/O 回调的话,则 setImmediate 一定会先执行。

fs.readFile('/path/to/file', () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);

  setImmediate(() => {
    console.log('immediate');
  });
});

// 输出 immediate -> timeout

上述代码会进入 Poll 阶段,等待执行 readFile 回调,添加 setImmediatesetTimeout 回调,查看 Poll 阶段处理步骤,此时 I/O 队列为空,setImmediate 队列存在,因此进入 Check 阶段执行回调,然后在下一次循环执行 setTimeout 的回调。

process.nextTick

虽然 process.nextTick 是异步 API 之一,但从技术上来说它不属于事件循环(EventLoop)的一部分。process.nextTick 会中断事件循环(EventLoop),不管在事件循环(EventLoop)的任何时刻,当前操作完成后(即执行栈为空时),会执行 nextTickQueue 的所有回调,然后再继续进行事件循环(EventLoop)。

NodeJS 对操作的定义:
an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.

案例:

const fs = require('fs');

process.nextTick(() => {
  console.log('nextTick1');
});

setTimeout(() => {
  console.log('setTimeout1');
  process.nextTick(() => {
    console.log('nextTick2');
  });
});

setTimeout(() => {
  console.log('setTimeout2');
});

fs.readFile('./index.js', () => {
  console.log('readFile');
});

执行过程:

  1. 在初始化代码执行完成后,执行栈为空,nextTickQueue 有回调,执行输出'nextTick1'
  2. 进入 Timer 阶段
    1. 执行输出 setTimeout1
    2. 执行栈为空,nextTickQueue 有回调,执行输出 'nextTick2'
    3. 执行输出 setTimeout2
  3. 进入 Poll 阶段,执行输出 readFile

解释:

  1. nextTick2setTimeout2 之前说明了不管什么时候,执行栈为空,先执行nextTickQueue
  2. nextTick2readFile 之前说明了仍然在本次循环中。

process.nextTick vs setImmediate

setImmediate 的含义是“立即”,容易让人以为比 process.nextTick 先执行,这个命名是一个历史问题。

实际上,上面已经提到过,setImmediate 是事件循环的一部分,它在循环即将结束的 Check 阶段执行,而 process.nextTick 无论何时,只要当前操作结束就会被执行,它们之间并没有太多联系。

关于微任务(microtask)

NodeJS 的官方文档并没有把 微任务(microtask) 写入事件循环,查阅了一些资料,猜测可能是以下原因:

微任务(microtask)是 WHATWG 中的概念,浏览器只有一个任务队列(task queue),任务(task)并无优先级,而微任务队列(microtask queue)提供了优先级。最初微任务(microtask)也是提供给 Promise/A+ 的 then 函数实现使用,后续更多的浏览器 API 被实现以微任务(microtask)执行。

NodeJS 实现事件循环时是多队列的,所有异步回调有着优先级调度。而对于 Promise.then 函数的实现,在上述事件循环的某个阶段后执行,也是符合 ES6 规范和Promise/A+ 规范的,除此之外并没有其他需要微任务(microtask)。

所以 NodeJS 使用了微任务(microtask)的语义,但并没有按 WHATWG 规范实现微任务队列。

但是在 NodeJS 11 后,修改了 process.nextTickPromise.then 的执行时机,并且增加 queueMicrotask API:

  1. timers: run nextTicks after each immediate and timer #22842.
  2. MacroTask and MicroTask execution order #22257.
  3. implement queueMicrotask #22951.

微任务(microtask)会在 process.nextTick 执行完后执行。

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant