为什么会有 Event Loop

简单来说 Event Loop 通过将请求分发到别的地方,使得 Node.js 能够实现非阻塞(non-blocking)I/O 操作。

Event Loop 工作原理

流程是这样的,你执行 node index.js 或者 npm start 之类的操作启动服务,所有的同步代码都会被执行,然后判断是否有 Active handle,如果没有就会停止。

  1. // index.js 执行完成之后立马停止
  2. console.log('Hello world')
  3. const express = require('express')
  4. const app = express()
  5. const port = 3000
  6. app.get('/', (req, res) => res.send('Hello world'))
  7. app.listen(port, () => console.log(Example app listening on port ${port}!))

// 这里运行的 app.listen 就是一个 active handle,有这个存在就相当于 Node.js 有理由继续运行下去,这样我们就进入了 Event Loop
Event Loop 包含了一系列阶段(phase),每个阶段都是只执行属于自己的任务(task)和微任务(micro task),这些阶段依次为:

  1. timer(定时器)
  2. pending callbacks(待定回调)
  3. idle,prepare
  4. poll(轮询)
  5. check(检测)
  6. close callbacks(关闭回调)

执行顺序是:

  1. 输入数据阶段(incoming data)
  2. 轮询阶段(poll)
  3. 检查阶段(check)
  4. 关闭事件回调阶段(close callbacks)
  5. 定时器检测阶段(timer)
  6. I / O 事件回调阶段(I / O callbacks)
  7. 闲置阶段(idle,prepare)
  8. 轮询阶段

    timer

    当你使用 setTimeout 和 setInterval 的时候,传入的回调函数就是在这个阶段执行。
  1. setTimeout(function () {
  2. console.log('Hello world') // 这一行在 timer 阶段执行
  3. }, 1000)

check

和 timer 阶段类似,当你使用 setImmediate 函数的时候,传入的函数回调就是在 check 阶段执行。

  1. setImmediate(function () {
  2. console.log('Hello world') // 这一行在 check 阶段执行
  3. })

poll

poll 阶段基本涵盖了其余所有情况,你写的大部分回调,如果不是上面两种(排除 micro task),那基本上就是在 poll 阶段执行了。

  1. // io 回调
  2. fs.readFile('index.html', 'utf8', function(err, data) {
  3. console.log('Hello world') // 在 poll 阶段执行
  4. })
  1. // http 回调
  2. http.request('http://example.com', function(res) {
  3. res.on('data', function() {})
  4. res.on('end', function() {
  5. console.log('Hello world') // 在 poll 阶段执行
  6. })
  7. }).end()

Node.js 事件循环
image.png

Microtask

我们可以想象每个阶段都有三个队列:

  1. 这个阶段同步任务队列
  2. 这个阶段 process.nextTick 队列
  3. 这个阶段 Promise 队列

Node.js 会采用先进先出的方式处理该阶段的任务,当所有同步任务都处理完毕后,会先清空 process.nextTick 队列,然后是 Promise 队列。如果在某一阶段一直递归调用 process.nextTick,会导致主线程一直停留在该阶段,表现类似于while(true)