Event Loop
事件循环是在主线程上完成的
事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行
setTimeout在 timers 阶段执行,而setImmediate在 check 阶段执行
下面是一个官方的例子:
|
|
事件循环分析:
- 第一轮事件,首先执行完同步代码之后,事件队列 push 进了 setTimeout, fs.readFile 两个异步操作。 主线程首先在 timers 阶段检查,没有到期的定时器,便离开这一阶段,同时没有可执行 I/O 操作,进入第二轮事件循环
- 由于读取小文件一般不会超过 100ms,所以此时依然没有到期的定时器,而 I/O 已经有callback返回,则 Poll 阶段就会得到结果,所以会执行 I/O 函数 ++readFile()++,而该 I/O 还未执行完的时候,定时器已经到期,但是必须执行完当前阶段,才会离开当前阶段继续往下执行
- 进入第三轮事件循环,此时 timers 检测到定时器到期,执行定时器,因此此时输出时间大概在
200ms 左右
关于 Macrotask / Microtask
Macrotask: setTimeout, setInterval, setImmediate, I/O, UI rendering
Microtask: process.nextTick, Promise, Object.observe, MutationObserver
- Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环
- 其中 process.nextTick 是所有异步任务里面最快执行的. Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列
- 特殊举例1:
|
|
注:结果不唯一, 1/2, 2/1 都可能,因为事件的取值范围在1毫秒到2147483647毫秒之间。
因此 setTimeout(()=>{}, 0) 和 setTimeout(()=>{}, 1) 是一样的, Node 做不到0毫秒,最少也需要1毫秒。
特殊举例2:
上述代码中,输出结果一定是: 2 1
注:setTimeout 是在 timers 阶段执行,setImmediate 是在 check 阶段执行。 该轮事件循环中,只有 I/O 函数 fs.readFile ,会先进入 I/O callback 阶段,然后进入 check 阶段,所以先执行了 setImmediate, 结束后第二轮循环,进入 timers 阶段,执行 setTimeout 定时器
- 特殊举例3:123456// 下面两行,次轮循环执行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:
|
|
上述输出结果为:3 1 4 5 2 6
注:
- process.nextTick 的回调会在 timers 阶段和I/O callbacks 阶段之间执行。执行同步代码结束,执行 process.nextTick 输出 3
- s1 是处于主线程的同步代码中,所以它的回调会排在 s2, s3 之前,但是三个 setImmediate s1, s2, s3处于同一轮事件循环中,因为第二个 setImmediate 执行的时候,check 阶段还没有过,所以此时依次输出 1 4 5
- 进入新一轮事件循环,先执行 process.nextTick 回调,输出 2, 再进入 check 阶段执行 setImmediate 输出 6
参考了 阮一峰 很多知识点的讲解,非常受用,也让自己对 Event Loop 有了更加清晰的理解
参考链接:Node 定时器