Node.js 之 Event Loop事件循环

509 阅读2分钟

什么是Event Loop

Node.js 可以处理很多请求任务(如网络/文件等),将这些任务(也叫回调函数)分成至少 6 类,按先后顺序调用,因此将事件分为六个阶段。Node.js 会不停的从 1 ~ 6 循环处理各种事件,这个过程叫做事件循环(Event Loop)

Event Loop的六个阶段

   ┌───────────────────────┐
┌─>│      1. timers        │  主要处理 setTimeout 任务
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │    2. I/O callbacks   │   该阶段不用管
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │    3. idle, prepare   │   该阶段不用管
│  └──────────┬────────────┘                ┌───────────────┐
│  ┌──────────┴────────────┐   停留时间最长  │   incoming:   │
│  │     4.  poll          │<───────────────┤  connections, │ 
│  └──────────┬────────────┘                │   data, etc.  │
│  ┌──────────┴────────────┐                └───────────────┘
│  │    5.  check          │  主要处理 setImmediate 任务
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤   6. close callbacks  │  该阶段不用管
   └───────────────────────┘

上图一共有六个阶段,每个阶段都有一个「先入先出队列」,队列里存储要执行的回调函数,当 Event Loop 达到某个阶段时,会在这个阶段进行一些「特殊的操作」,然后执行这个阶段的队列里的所有回调函数,以下情况会结束这个阶段,进入下一个阶段:

  • 队列的操作全被执行完了
  • 执行的回调函数的个数到达设定的最大值,Event Loop必须进入下一个阶段。

我们只关心以下三个阶段:

  • timer计时器。setTimeout回调在此阶段执行
  • poll轮询。检查系统事件。大部分时间,Node.js都停在poll轮询阶段。大部分事件都在poll阶段被处理,如文件,网络请求。一般在poll阶段有规定最大等待时间,比如停留3s后,还是会进入check阶段,再重新循环。
  • check检查。setImmediate回调。

nextTick (只有Node.js里有,浏览器无)

问:process.nextTick(fn) 的 fn 会在什么时候执行呢?

答:

  • 在 Node.js 11 之前,会在每个阶段的末尾集中执行(俗称队尾执行)。
  • 在 Node.js 11 之后,会在每个阶段的任务间隙执行(俗称插队执行)。

浏览器跟 Node.js 11 之后的情况类似。可以用 window.queueMicrotask 模拟 nextTick,进行插队执行。

Promise

问:Promise.resolve(1).then(fn) 的 fn 会在什么时候执行?

答:转换成 process.nextTick(fn) 去判断。

async / await

改写成 Promise,再转成nextTick写法

async1()
await async2()
fn()

在 await 之前的代码,会同步执行,在 await 后面的代码,可以改写为async2().then(fn()),再改写为:先执行 async2(),再nextTick(fn())

面试题1

在 Node.js 里运行以下代码会输出什么?

setTimeout(() => {
  console.log('s1')
})
​
setImmediate(() => {
  console.log('s2')
})

答案:先输出s1还是s2都有可能,这要看是先启动JS引擎,还是先启动libuv。

  • 如果先启动了JS引擎,那么JS执行到setTimeout会塞入timers阶段,将setImmediate塞入check阶段,然后再启动libuv开始事件循环,这时候先经历timers阶段,输出s1,再经历check阶段,输出s2.
  • 如果先启动了libuv,那么大部分时间都停留在poll阶段,JS启动后将setTimeout会塞入timers阶段,将setImmediate塞入check阶段,poll->check->timers,先输出s2,再输出s1

面试题2

async function async1(){
    console.log('1')                   
    async2().then(()=>{    // f2
      console.log('2')
    })
}
async function async2(){
    console.log('3')                    // 3
}
console.log('4')           
setTimeout(function(){      // f1
    console.log('5') 
},0)  
async1();
new Promise(function(resolve){   // f3
    console.log('6')                   
    resolve();
}).then(function(){   // f4
    console.log('7')
})
console.log('8')                        // 5  
//4 1 3 6 8 2 7 5 

输出4 1 3 6 8 2 7 5。执行过程如下:

同步:

  • 先打印输出4
  • 把 函数 f1 放到 任务队列
  • 执行async1:打印输出1,执行 async2 ,打印输出3,将 f2 放入 任务队列里
  • 立刻执行 f3 ,打印输出6, resolve()使得 f4 放入 任务队列里
  • 打印输出8。此时宏任务队列 [ f1 ],微任务队列 [ f2,f4 ] 异步:
  • 先执行微任务队列,执行 f2,打印输出2
  • 执行 f4 ,打印输出7
  • 执行宏任务队列里的 f1 ,打印输出5

参考阅读