当前位置: 首页 > 文档资料 > Node.js 调试指南 >

代码篇 - Event Loop

优质
小牛编辑
132浏览
2023-12-01

Event Loop 是 Node.js 最核心的概念,所以理解 Event Loop 如何运作对于写出正确的代码和调试是非常重要的。比如考虑以下代码:

  1. setTimeout(() => {
  2. console.log('hi')
  3. }, 1000)
  4. ...

我们期望程序运行 1s 后打印出 hi,但是实际情况可能是远大于 1s 后才打印出 hi。这个时候如果理解 Event Loop 就可以轻易发现问题,否则任凭怎么调试都是发现不了问题的。

关于 Event Loop 运作原理这篇文章(https://cnodejs.org/topic/5a9108d78d6e16e56bb80882)从 Node.js 和 Libuv 源码分析的非常透彻,本节就不赘述了。本节就拿出六道题来补充一下,放出一张关于 Event Loop 非常直观的图:

Event Loop - 图1

绿色小块是 macrotask(宏任务),macrotask 中间的粉红箭头是 microtask(微任务)。

3.6.1 题目一

  1. setTimeout(() => {
  2. console.log('setTimeout')
  3. }, 0)
  4. setImmediate(() => {
  5. console.log('setImmediate')
  6. })

运行结果:

  1. setImmediate
  2. setTimeout

或者:

  1. setTimeout
  2. setImmediate

为什么结果不确定呢?

解释:setTimeout/setInterval 的第 2 个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:

  1. timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数。
  2. timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数。

再看个例子:

  1. setTimeout(() => {
  2. console.log('setTimeout')
  3. }, 0)
  4. setImmediate(() => {
  5. console.log('setImmediate')
  6. })
  7. const start = Date.now()
  8. while (Date.now() - start < 10);

运行结果一定是:

  1. setTimeout
  2. setImmediate

3.6.2 题目二

  1. const fs = require('fs')
  2. fs.readFile(__filename, () => {
  3. setTimeout(() => {
  4. console.log('setTimeout')
  5. }, 0)
  6. setImmediate(() => {
  7. console.log('setImmediate')
  8. })
  9. })

运行结果:

  1. setImmediate
  2. setTimeout

解释:fs.readFile 的回调函数执行完后:

  1. 注册 setTimeout 的回调函数到 timer 阶段。
  2. 注册 setImmediate 的回调函数到 check 阶段。
  3. event loop 从 pool 阶段出来继续往下一个阶段执行,恰好是 check 阶段,所以 setImmediate 的回调函数先执行。
  4. 本次 event loop 结束后,进入下一次 event loop,执行 setTimeout 的回调函数。

所以,在 I/O Callbacks 中注册的 setTimeout 和 setImmediate,永远都是 setImmediate 先执行。

3.6.3 题目三

  1. setInterval(() => {
  2. console.log('setInterval')
  3. }, 100)
  4. process.nextTick(function tick () {
  5. process.nextTick(tick)
  6. })

运行结果:setInterval 永远不会打印出来。

解释:process.nextTick 会无限循环,将 event loop 阻塞在 microtask 阶段,导致 event loop 上其他 macrotask 阶段的回调函数没有机会执行。

解决方法通常是用 setImmediate 替代 process.nextTick,如下:

  1. setInterval(() => {
  2. console.log('setInterval')
  3. }, 100)
  4. setImmediate(function immediate () {
  5. setImmediate(immediate)
  6. })

运行结果:每 100ms 打印一次 setInterval。

解释:process.nextTick 内执行 process.nextTick 仍然将 tick 函数注册到当前 microtask 的尾部,所以导致 microtask 永远执行不完; setImmediate 内执行 setImmediate 会将 immediate 函数注册到下一次 event loop 的 check 阶段,而不是当前正在执行的 check 阶段,所以给了 event loop 上其他 macrotask 执行的机会。

再看个例子:

  1. setImmediate(() => {
  2. console.log('setImmediate1')
  3. setImmediate(() => {
  4. console.log('setImmediate2')
  5. })
  6. process.nextTick(() => {
  7. console.log('nextTick')
  8. })
  9. })
  10. setImmediate(() => {
  11. console.log('setImmediate3')
  12. })

运行结果:

  1. setImmediate1
  2. setImmediate3
  3. nextTick
  4. setImmediate2

注意:并不是说 setImmediate 可以完全替代 process.nextTick,process.nextTick 在特定场景下还是无法被替代的,比如我们就想将一些操作放到最近的 microtask 里执行。

3.6.4 题目四

  1. const promise = Promise.resolve()
  2. .then(() => {
  3. return promise
  4. })
  5. promise.catch(console.error)

运行结果:

  1. TypeError: Chaining cycle detected for promise #<Promise>
  2. at <anonymous>
  3. at process._tickCallback (internal/process/next_tick.js:188:7)
  4. at Function.Module.runMain (module.js:667:11)
  5. at startup (bootstrap_node.js:187:16)
  6. at bootstrap_node.js:607:3

解释:promise.then 类似于 process.nextTick,都会将回调函数注册到 microtask 阶段。上面代码会导致死循环,类似前面提到的:

  1. process.nextTick(function tick () {
  2. process.nextTick(tick)
  3. })

再看个例子:

  1. const promise = Promise.resolve()
  2. promise.then(() => {
  3. console.log('promise')
  4. })
  5. process.nextTick(() => {
  6. console.log('nextTick')
  7. })

运行结果:

  1. nextTick
  2. promise

解释:promise.then 虽然和 process.nextTick 一样,都将回调函数注册到 microtask,但优先级不一样。process.nextTick 的 microtask queue 总是优先于 promise 的 microtask queue 执行。

3.6.5 题目五

  1. setTimeout(() => {
  2. console.log(1)
  3. }, 0)
  4. new Promise((resolve, reject) => {
  5. console.log(2)
  6. for (let i = 0; i < 10000; i++) {
  7. i === 9999 && resolve()
  8. }
  9. console.log(3)
  10. }).then(() => {
  11. console.log(4)
  12. })
  13. console.log(5)

运行结果:

  1. 2
  2. 3
  3. 5
  4. 4
  5. 1

解释:Promise 构造函数是同步执行的,所以先打印 2、3,然后打印 5,接下来 event loop 进入执行 microtask 阶段,执行 promise.then 的回调函数打印出 4,然后执行下一个 macrotask,恰好是 timer 阶段的 setTimeout 的回调函数,打印出 1。

3.6.6 题目六

  1. setImmediate(() => {
  2. console.log(1)
  3. setTimeout(() => {
  4. console.log(2)
  5. }, 100)
  6. setImmediate(() => {
  7. console.log(3)
  8. })
  9. process.nextTick(() => {
  10. console.log(4)
  11. })
  12. })
  13. process.nextTick(() => {
  14. console.log(5)
  15. setTimeout(() => {
  16. console.log(6)
  17. }, 100)
  18. setImmediate(() => {
  19. console.log(7)
  20. })
  21. process.nextTick(() => {
  22. console.log(8)
  23. })
  24. })
  25. console.log(9)

运行结果:

  1. 9
  2. 5
  3. 8
  4. 1
  5. 7
  6. 4
  7. 3
  8. 6
  9. 2

process.nextTick、setTimeout 和 setImmediate 的组合,请读者自行推理吧。

3.6.7 参考链接

上一节:3.5 Rust Addons

下一节:4.1 Source Map