Node.js 中的 Event Loop

Event Loop I/O:读写操作、输入输出、请求响应

1. setImmediate()

  • Check 阶段被执行
  • 不会检查函数队列,而是检查 I/O 队列,当所有 I/O 在当前循环结束后会执行
  • 只要 poll 队列为空,代码被 setImmediate(),无论是否有 timers 达到下限时间,setImmediate() 的代码都先执行。

2. setTimeout(fn, 0)

  • Timer 阶段被执行
  • 回调会被安排在 Timer 队列中
  • The timer phase is the first phase but is called after the I/O phase as well as the Check phase.

2.2 setImmediate() 和 setTimeout(fn, 0) 为什么有时候顺序随机?

Node 中执行下面的代码,会出现两种输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})

// output
setImmediate
setTimeout

// output
setTimeout
setImmediate

一次循环首先进入的是 timers 阶段,进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。所以如果进入之前一毫秒已经过去了,那么 setTimeout 的回调会首先执行。setTimeout 最小值是 >= 1ms/4ms0 会被转成 >= 1ms/4ms 根据具体情况确定

而下面这样就只会出现唯一的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//index.js
var fs = require('fs');
fs.readFile("my-file-path.txt", function() {
setTimeout(function(){
console.log("SETTIMEOUT");
});
setImmediate(function(){
console.log("SETIMMEDIATE");
});
});

//run it
node index.js

//output (always)
SETIMMEDIATE
SETTIMEOUT

因为 fs.readFile 的回调是在 poll 阶段执行的,当其回调执行完毕之后,poll 队列为空,而 setTimeout 入了 timers 队列,此时有代码被 setImmediate(),于是时间循环进入 check 阶段执行回调,之后再下一个时间循环再进入 timers 阶段。
整个顺序是:

  • 循环开始,没有 timer,没有 I/O 回调
  • fs.readFile 执行,把回调加入 I/O Callbacks
  • 进入 poll 阶段,等待文件读取
  • 第二轮循环
  • I/O 回调,进入 I/O Callbacks 阶段执行 fs.readFile 的回调
  • 随后循环进入 check 阶段,发现有 setImmediate 回调,执行
  • 之后下一轮循环才又回到 timers 阶段,这时才执行 setTimeout 的回调

3. process.nextTick()

  • 当前操作结束后就执行,和 Event Loop 状态无关,几乎是同步的,在本轮循环就安排执行
  • 安排在 nextTickQueue
  • 会阻塞 I/O,阻塞 Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var fs = require('fs');
console.log('Start');
Promise.resolve().then(() => console.log('PR1'));
setTimeout(() => console.log('TO1'), 0);
Promise.resolve().then(() => console.log('PR2'));
setImmediate(() => console.log('IM1'));
process.nextTick(() => console.log('NT1'));
fs.readFile("./profile", function() {
setTimeout(function(){
console.log("SETTIMEOUT");
});
process.nextTick(() => console.log('NT2'));
setImmediate(function(){
console.log("SETIMMEDIATE");
});
});
setImmediate(() => console.log('IM2'));
process.nextTick(() => console.log('NT3'));
setImmediate(() => console.log('IM3'));
Promise.resolve().then(() => console.log('PR3'));
setImmediate(() => console.log('IM4'));
console.log('Done');
setTimeout(() => console.log('STDone'), 1500);

// output
Start // i---1
Done // i---1
NT1
NT3
PR1
PR2
PR3
TO1
NT2
IM1
IM2
IM3
IM4
SETIMMEDIATE
SETTIMEOUT
STDone

// 在浏览器中 IM 永远会比 T0 先执行

4. Promise.resolve().then

  • 这个其实没有把回调安排到下一个循环和 process.nextTick 类似,在本轮循环就安排了
  • 会进入异步任务里面的”微任务”(microtask)队列
  • 同步任务 结束后 nextTickQueue 结束后 microTaskQueue

其他:

  1. Promises do swallow exceptions
  2. 创建了两个 Promises 出来,并且什么都没做就抛了第一个,Don’t create promises only to throw them away.
  3. 上述讨论也就在 Node 环境下
    在浏览器中,基本只剩 setTimeoutPromise.resolve().then
    更多的需要关注浏览器的 EventLoop