写点什么

NodeJS 事件循环

作者:shinji
  • 2022 年 2 月 24 日
  • 本文字数:2870 字

    阅读完需:约 9 分钟

NodeJS事件循环

什么是事件循环

事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。浏览器事件循环与 Nodejs 事件循环的区别

Nodejs 事件循环的简化流程

图片来自 Nodejs 官网


阶段概述

阶段代表的含义

  1. timers 阶段:主要是执行宏任务注册的回调函数,诸如 setTimeout,setInterval 等的回调函数。

  2. pending callbacks:此阶段执行某些系统操作的回调,例如 TCP 错误。 举个例子,如果 TCP 套接字在尝试连接时收到 ECONNREFUSED,则某些* nix 系统希望等待报告错误。 这将会在 pending callbacks 阶段排队执行

  3. idle/prepare:主要是系统级回调,无需关心

  4. poll:i/o 阶段,例如 fs.readFiles...等 I/O 操作

  5. check:setImmediate 注册的回调函数将在这个这个阶段执行

  6. close callbacks: 这个阶段主要执行一些关闭函数,诸如 socket.on('close', ... )等方法。Nodejs 每一次的事件循环始于 timers 阶段,结束于 close callbacks 阶段,当每轮事件循环结束后会立刻开启一轮新的事件循环,这就是 EventLoop。

需要注意的点

  1. 根据上图能看到,poll 轮训阶段是处理 I/O 相关操作的,所以在 poll 轮训中是有机会存在轮训暂停,等待新的 I/O 任务的时候,如果有新的 I/O 任务,就会执行 I/O 操作。

  • 什么时候会停留?当 timers 阶段没有注册的回调函数,以及 check 阶段没有注册的回调函数的时候,事件循环会在 poll 阶段进行短暂的停留,等待新的 I/O 操作。

  • 什么时候不停留?当 timers 阶段和 check 阶段的回调队列不为空的时候就会不做暂停,事件循环会从 poll 轮训阶段跳转到 check 阶段

  1. Nodejs 的事件循环同样分为微任务宏任务,在 Nodejs 的微任务中,process.nextTick()的优先级是高于 Promise 的。

代码示例

Nodejs 如何进行事件循环的

示例 1
console.log('start')setTimeout(() => {    console.log('1')})setTimeout(() => {    console.log('2')})console.log('end')// 输出 start end 1 2// 解释:当代码输入完毕开始运行,console.log('start')属于同步任务,会直接执行。当遇到第一个setTimeout函数会将回调函数注册到 timers 阶段的回调队列中,第二个setTimeout同理,然后再遇到第二个console函数,同步执行。 同步执行完毕,进入事件循环,根据上面概述,事件循环始于timers阶段,且这两个setTimeout都没有设置ms参数,所以会一次根据先进先出的规则输出 1和2// setTimeout第二个参数 不写或者传人0都会被node强制改为4ms
复制代码
示例 2
setTimeout(() => {    console.log(1)}, 0)
setImmediate(() => { console.log(2)})
function sleep(duration) { let start = new Date().getTime() while(new Date().getTime() - start < duration) { continue } }sleep(1000)// 1, 2// 2, 1// 以上两种情况都有可能。// 解释:代码输入后,遇到第一个setTimeout函数,会将其回调注册到timers阶段,之后遇到setImmediate函数,会将其回调注册到check阶段,之后执行同步sleep方法,所以上面的代码会在1000ms后才会有输出// setTimeout 函数的 回调时间设置为0,这个上面讲过,会被node强制修改掉,所以在当前event loop中如果还没到回调时间,那么就会跳过 timers阶段,然后走到checke阶段,这种情况下就会先输出2, 再输出1, 如果当前event loop过程中,setTimeout的回调函数到了,那么就会先输出1, 再输出2。// 为什么会出现这种情况,完全取决于你当前机器的状态。
复制代码
示例 3
const fs = require('fs')fs.readFile('./index.html', () => {    setTimeout(() => {        console.log(1)    }, 0)        setImmediate(() => {        console.log(2)    })})// 输出 2, 1// 疑问: 这段代码不管怎么样都会输出2, 1。同样都是setTimeout 和 setImmediate, 看起来与示例2的代码没有太大的区别, 为什么会一定输出2, 1呢?// 解释: 由于这段代码 只有一个同步读取文件的函数方法,所以这段代码输入后,时间循环会直接跳入到poll轮训阶段,poll阶段主要出里I/O操作,所以这段代码在读取完文件后,会执行后续的回调函数,会将console.log(1)注册到timers阶段,console.log(2)注册到checke阶段。然后当前poll阶段的回调执行完毕,那么时间循环会跳转到check阶段(看流程图),跳转到check阶段后,发现check阶段已经有注册过的console.log(2)函数,那么会先进先出的执行回调函数。check阶段的回调执行完后,当前event loop执行完毕。然后从timers开起新的event loop, 发现timers阶段已经有回调,那么就执行回调,所以这段代码的输出一定会是2, 1这样子。
复制代码
示例 4
setTimeout(() => {    console.log(1)}, 60)process.nextTick(() => {console.log(2)})setImmediate(() => {console.log(3)})process.nextTick(() => {console.log(4)})
// 输出: 2 4 3 1// 解释: 都是异步API// 1. 将console.log(1) 注册到timers阶段// 2. 将console.log(2) 微任务会被注册到微任务的事件队列中// 3. 将console.log(3) 注册到check阶段// 4. 将console.log(4) 同样微任务会被注册到微任务的事件队列中// 由于微任务优先级高于宏任务,当前微任务队列里有两个注册的回调函数,根据FIFO规则,所以先输出2, 再输出4。微任务队列清空,开始执行宏任务,事件循环从timers阶段开始,但是由于 设置了60ms的回调时间,所以时间没到,那么时间循环就会掉过当前timers阶段,跳入到check阶段,当前阶段有回调函数,接着就输出3。当回调的60ms还没到的时候,node会不停的进行事件循环的扫描,一轮又一轮,当60ms到了,那么就会执行timers阶段的回调函数,此时会输出1。// 至此输出 2 4 3 1
复制代码
示例 5
setTimeout(() => {    console.log(1)}, 60)setImmediate(() => {console.log(2)})process.nextTick(() => {console.log(3)})Promise.resolve().then(() => console.log(4));(() => console.log(5))()
// 输出 5 3 4 2 1// 解释: 先输出5就不用解释了,nextTick优先级高于Promise,所以输出3, 4也没有问题,剩下的输出2,1。具体可以参考示例4
复制代码
示例 6
process.nextTick(() => {console.log(1)})Promise.resolve().then(() => console.log(2))process.nextTick(() => {console.log(3)})Promise.resolve().then(() => console.log(4))
// 输出: 1 3 2 4// 解释: 不解释
复制代码
示例 7
setTimeout(() => {    console.log(1)}, 50)process.nextTick(() => {console.log(2)})setImmediate(() => {console.log(3)})process.nextTick(() => {    setTimeout(() => {        console.log(4)    }, 1000)})
// 输出:2 3 1 4// 解释:根据之前示例的讲解,先执行微任务,输出2, 然后在第二个微任务回到中执行了setTimeout函数,此时timers的队列中存在了2个回调函数。微任务执行完毕接下来执行宏任务,由于timers两个阶段的函数都设置了回调时间,所以优先会执行check阶段队列中的回调,所以输出3,接下来根据回调时间的快慢执行,先输出1,再输出4。
复制代码

参考资料

其他请参考 Nodejs 官网https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/

发布于: 刚刚阅读数: 2
用户头像

shinji

关注

fly me to the moon 2020.07.07 加入

还未添加个人简介

评论

发布
暂无评论
NodeJS事件循环