nodejs的事件循环

262 阅读4分钟

浏览器中 JS的事件循环相信大家都很熟悉。nodejs 是和操作系统打交道, 所以学习起来会有一点难度。

node事件循环图

a08efce7c0430f12848a65b3260bd1b.png

这里面会涉及到一些多线程, 本文只讨论跟 JS 线程有关的信息。

执行过程

代码的运行首先会经过正常的执行, 当同步代码执行完毕之后就会去看还有没有其余线程的信息, 比如 setTimeout 这里就进入上图中的 event loop 右边的循环执行。

每一次循环会经过 timers >> pending callback >> idle prepare >> poll >> check >> close callbacks 这六个阶段(一次循环)

这六个都是队列

这里主要讨论 timers、poll、check 队列

timers (计时器队列)

timers 队列主要负责处理计时器的回调函数

poll (轮询队列)

poll 存放除了 timers、check 绝大部分回调都会放在该队列,比如:读写文件内容的回调函数,监听用户的请求的回调函数。

import { readFile } from 'fs';

readFile('/etc/passwd', (err, data) => { // 该回调函数进入的是 poll 队列
  if (err) throw err;
  console.log(data);
});

poll队列的运作方式

1、如果 poll中有回调,依次执行回调,直到清空队列。

2、如果 poll中没有回调

  • 等待其它队列中出现回调,结束该阶段,进入下一阶段

  • 如果其它队列也没有回调,持续等待,知道出现回调为止

check (检查阶段)

使用 setImmediate 的回调会直接进入这个队列。

setImmediate 函数可以当做是: setTimeout(callback,0)

setImmediate(() => {
    console.log(123)
})

setTimeout(() => {
    console.log(345)
}, 0)
// 输出的顺序 有个能345 在前也有可能 123
// 因为 setTimeout 不存在取到 0 只能 >0, 所以当进入 timers 队列时不一定存在 回调函数。 

setImmediate所以它一定是异步的, 但是异步回调函数进入的是 check 队列。

现在大致了介绍 timer、poll、check 队列,下面用一段代码解释执行的顺序。

const fs = require('fs')

fs.readFile('./index.js', 'utf-8', function test () {
    console.log('读取文件的回调函数')

    setTimeout(() => {
        console.log(1)
    }, 0)

    setImmediate(() => {
        console.log(2)
    })
})

这段代码的执行结果为

读取文件的回调函数
2
1

使用事件循环的角度来分析为什么是这个输出结果:

代码执行到读取文件, 当文件读取完时会触发 test 回调函数, test回调函数加入 poll 队列, poll队列执行 test回调函数, 输出 读取文件的回调函数, 然后执行到了 setTimeout 那么会通知计时器线程监听(当时间到达时, 才会将setTimeout的回调函数加入 timers 队列),继续执行到 setImmediate直接将回调函数加入check队列, 然后test执行完毕,此时poll队列执行完毕, 然后监听其它队列是否有需要执行的信息, 发现check队列需要执行,然后就到了check队列执行输出2check也执行完毕然, 然后此时 计时器timers 加入了回调函数(加入的时间点并不能确定), 所以当 check 执行完毕时又进入了行的事件循环, 此时 timers 有需要执行的信息, 然后输出 1, 当 timers 执行完毕,系统发现没有什么需要执行的就结束了程序执行。

解释图中的 NextTick、Promise

NextTick、Promise 也是队列,但是它们不同,上面的事件队列可以说是 宏任务, 它们两则是 微任务 它们并不存在事件循环中, 它们两有一个优先级:先执行 NextTick 然后在执行 Promise

NextTick、Promise 怎么配合事件循环的

用一句话说就是:事件循环中,每次打算执行一个 回调函数 必须先清空 NextTick、Promise 列表

还是使用一段代码解释:

process.nextTick(() => {
    console.log(1)
    process.nextTick(() => {
        console.log(2)
    })
})

console.log(3)

Promise.resolve().then(() => {
    console.log(4)
    process.nextTick(() => {
        console.log(5)
    })
})

setTimeout(() => {
    console.log(6)
})

输出的结果为 3 1 2 4 5 6

  1. 先输出同步代码 3
  2. 进入 NextTick 输出 1, 然后又往 NextTick 加入回调函数, 由于优先级问题马上执行 NextTick 输出2
  3. NextTick 队列清空后, 在执行 Promise 队列输出 4,然后又往 NextTick 队列加入回调函数, 当 Promise 执行完毕时,此时 NextTick 有信息马上执行 输出 5
  4. 此时 NextTick、Promise 队列已经清空了然后进入事件循环 timers 输出 6

总结

如果想要最快的执行异步回调函数,那么可以使用将回调函数加入 NextTikc 队列的api, 其次是 Promise, nodejs 的事件循环了解之后对我们的日常工作起到一定的好处。