Event-Loop

Event Loop

  • 事件循环是在主线程上完成的

  • 事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行

  • setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行

image

下面是一个官方的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');
const timeoutScheduled = Date.now();
// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms`);
}, 100);
// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('test.js', () => {
const startCallback = Date.now();
while (Date.now() - startCallback < 200) {
// 什么也不做
}
});
事件循环分析:
  1. 第一轮事件,首先执行完同步代码之后,事件队列 push 进了 setTimeout, fs.readFile 两个异步操作。 主线程首先在 timers 阶段检查,没有到期的定时器,便离开这一阶段,同时没有可执行 I/O 操作,进入第二轮事件循环
  2. 由于读取小文件一般不会超过 100ms,所以此时依然没有到期的定时器,而 I/O 已经有callback返回,则 Poll 阶段就会得到结果,所以会执行 I/O 函数 ++readFile()++,而该 I/O 还未执行完的时候,定时器已经到期,但是必须执行完当前阶段,才会离开当前阶段继续往下执行
  3. 进入第三轮事件循环,此时 timers 检测到定时器到期,执行定时器,因此此时输出时间大概在
    200ms 左右

关于 Macrotask / Microtask
  • Macrotask: setTimeout, setInterval, setImmediate, I/O, UI rendering

  • Microtask: process.nextTick, Promise, Object.observe, MutationObserver

  1. Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环
  2. 其中 process.nextTick 是所有异步任务里面最快执行的. Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列
  • 特殊举例1:
1
2
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

注:结果不唯一, 1/2, 2/1 都可能,因为事件的取值范围在1毫秒到2147483647毫秒之间。

因此 setTimeout(()=>{}, 0) 和 setTimeout(()=>{}, 1) 是一样的, Node 做不到0毫秒,最少也需要1毫秒。


特殊举例2:

1
2
3
4
5
6
const fs = require('fs');
fs.readFile('test.js', () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
});

上述代码中,输出结果一定是: 2 1

注:setTimeout 是在 timers 阶段执行,setImmediate 是在 check 阶段执行。 该轮事件循环中,只有 I/O 函数 fs.readFile ,会先进入 I/O callback 阶段,然后进入 check 阶段,所以先执行了 setImmediate, 结束后第二轮循环,进入 timers 阶段,执行 setTimeout 定时器


  • 特殊举例3:
    1
    2
    3
    4
    5
    6
    // 下面两行,次轮循环执行
    setTimeout(() => console.log(1));
    setImmediate(() => console.log(2));
    // 下面两行,本轮循环执行
    process.nextTick(() => console.log(3));
    Promise.resolve().then(() => console.log(4));

思考: 按照上述例子,此处为什么不是输出 ++3 4 1 2++ 或 ++3 4 2 1++ ? setTimeout/setImmediate 不是应该都可能有先后顺序吗?

注:因为此时 setTimeout,setImmediate 已经是处于第二轮事件循环队列中了,已经执行过一轮事件循环,node现阶段再快,1ms 也近乎是极限了,所以 timers 开始执行 setTimeout, 然后到了 check 再执行了 setImmediate


  • 例 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setImmediate(function () { // s1
console.log(1);
setImmediate(function () {
console.log(6);
})
process.nextTick(function () {
console.log(2);
});
});
process.nextTick(function () {
console.log(3);
setImmediate(function () { // s2
console.log(4);
})
setImmediate(function () { // s3
console.log(5);
})
});

上述输出结果为:3 1 4 5 2 6

注:

  1. process.nextTick 的回调会在 timers 阶段和I/O callbacks 阶段之间执行。执行同步代码结束,执行 process.nextTick 输出 3
  2. s1 是处于主线程的同步代码中,所以它的回调会排在 s2, s3 之前,但是三个 setImmediate s1, s2, s3处于同一轮事件循环中,因为第二个 setImmediate 执行的时候,check 阶段还没有过,所以此时依次输出 1 4 5
  3. 进入新一轮事件循环,先执行 process.nextTick 回调,输出 2, 再进入 check 阶段执行 setImmediate 输出 6

参考了 阮一峰 很多知识点的讲解,非常受用,也让自己对 Event Loop 有了更加清晰的理解

参考链接:Node 定时器