详解NodeJS事件循环

官网:node官网-事件循环

浏览器中的事件循环是由HTML规范来定义,之后由各浏览器厂商实现的,而node中的事件循环的定义与实现均由libuv引擎完成。

node使用chrome v8引擎作为js解释器,v8引擎分析代码后,主线程立即执行同步任务,而异步任务则由libuv引擎驱动执行,而且不同异步任务的回调事件会放在不同的队列中等待主线程执行,不再是简单的宏任务队列和微任务队列。因此在nodeJS中,虽然程序运行表现出的整体状态与浏览器中传统的js大致相同,先同步后异步,但是对于异步的部分,node则依靠libuv引擎来进行更复杂的管理。

宏任务队列和微任务队列

六个基本阶段(六个宏任务队列)

在这里插入图片描述

  1. timers:计时器阶段,处理setTimeout()和setInterval()定时器的回调函数
  2. pending callbacks :待定回调阶段,用于处理系统级别的错误信息,例如 TCP 错误或者 DNS 解析异常
  3. idle,prepare:仅在内部使用,可以忽略不计
  4. poll:轮询阶段,等待I/O事件(如网络请求或者文件I/O等)的发生,然后执行对应的回调函数,并且会处理定时器相关的回调函数。如果没有任何I/O事件发生,此阶段可能会使事件循环阻塞
  5. check:检查阶段,处理 setImmediate() 的回调函数。check 的回调优先级比 setTimeout 高,比微任务要低
  6. close callbacks:关闭回调阶段,处理一些关闭的回调函数,比如 socket.on(‘close’)

nextTick队列(微任务队列)

该事件队列独立于6个阶段的事件队列之外,用于存储 process.nextTick() 的回调函数。

microTask队列(微任务队列)

该事件队列也独立于6个阶段的事件队列之外,用于存储 Promise(Promise.then()、Promise.catch()、Promise.finally())的回调函数。

NodeJS事件循环流程

以上六个基本阶段和两个独立的事件队列构成了node事件循环的核心部分,在一次循环迭代的流程中,需要注意:

  1. nextTick队列、microTask队列中的任务穿插于6个阶段之间进行,每个阶段进行前会先执行并清空nextTick队列、microTask队列中的回调任务(可以理解为一次循环迭代至少处理6次nextTick队列和microTask队列中的任务)
  2. nextTick队列、microTask队列执行的次数在Node v11.x版本前后有一些差异,(上文中的至少很有深意),具体如下:
    a. Node版本小于11时,nextTick队列、microTask队列中的任务只会在6个阶段之间进行,因此一次循环迭代最多处理6次这两个队列
    b. Node版本大于11时,任何一个阶段的事件队列中任务之间都会处理一次这两个队列,因此一次循环迭代至少处理6次这两个队列,上限则受各个阶段总任务数影响而不固定
    c. 上述2个版本之间的区别,被认为是一个应该要修复的bug,因此在v11.x之后,node修改了nextTick队列、microTask队列的处理时机。从宏、微任务的角度看,修复后的流程和传统js的事件循环保持了一致
  3. nextTick队列中任务的优先级高于microTask队列

setTimeout() 与 setlmmediate() 的特殊情况

我们知道 setTimeout()的回调是在 timers阶段执行,setImmediate()的回调是在 check阶段执行,并且事件循环是从 timers阶段开始的,那么 setTimeout()的回调一定会先于 setImmediate()的回调执行吗?答案是不一定。在只有这两个函数且近乎同时触发的情况下,它们回调的执行顺序不是固定的(受调用时机、计算机性能影响)。下面是一个例子:

// 示例1(node v12.16.3)
setTimeout(() => {console.log("setTimeout");
});setImmediate(() => {console.log("setImmediate");
});// 结果:
// setTimeout -> setImmediate
// 或
// setImmediate -> setTimeout

上面示例1中的这段代码输出结果就是不固定的,这是因为这种情况下回调不一定完全准备好了。因为主线程没有同步代码需要执行,程序一开始就进入了事件循环。这时setTimeout()的回调并不是一定完全准备好了,因此就可能会在第一次循环迭代的check阶段中执行setImmediate()的回调,再到第二次循环迭代的timers阶段执行setTimeout()的回调;同时也有可能setTimeout()的回调一开始就准备好了,这样就会按照先setTimeout()再setImmediate()的顺序执行回调。由此就造成了输出结果不固定的现象。

有以下两种方法可以使输出顺序固定:
① 人为添加同步代码的延时器,保证回调都准备好了(延时器的时长设定可能会受机器运行程序时的性能影响,因此该方法严格意义上并不能100%固定顺序)。
② 将这两个方法放入pending callbacks、idle,prepare、poll阶段中任意一个阶段即可,因为这些阶段执行完后是一定会先到check再到下一个迭代的timers。由于pending callbacks、idle,prepare阶段都偏向于系统内部,因此一般可以放入poll阶段中使用。

如下示例2,我们人为加上一个2000ms的延时器,输出的结果就固定了,如下所示:

//示例2(node v12.16.3)
setTimeout(() => {console.log("setTimeout");
});setImmediate(() => {console.log("setImmediate2");
});const sleep = (delay) => {const startTime = +new Date();while (+new Date() - startTime < delay) {continue;}
};
sleep(2000);// 结果:setTimeout -> setImmediate

如下示例3,我们将函数放入文件I/O的回调中,输出的结果也就固定了,如下所示:

//示例3(node v12.16.3)
const fs = require("fs");fs.readFile("./fstest.js", "utf8", (err, data) => {setTimeout(() => {console.log("setTimeout");});setImmediate(() => {console.log("setImmediate");});
});// 结果:setImmediate -> setTimeout 

NodeJS事件循环示例

console.log('1'); //1层同步//1层timers,setTimeout1
setTimeout(function() {console.log('2'); //2层同步process.nextTick(function() {console.log('3'); //2层nextTick队列})new Promise(function(resolve) {console.log('4'); //2层同步resolve();}).then(function() {console.log('5'); //2层microTask队列})
})process.nextTick(function() {console.log('6'); //1层nextTick队列
})new Promise(function(resolve) {console.log('7'); //1层同步resolve();
}).then(function() {console.log('8'); //1层microTask队列
})//1层timers,setTimeout2
setTimeout(function() {console.log('9'); //2层同步process.nextTick(function() {console.log('10'); //2层nextTick队列})new Promise(function(resolve) {console.log('11'); //2层同步resolve();}).then(function() {console.log('12'); //2层microTask队列})
})console.log('13'); //1层同步//(node v12.16.3)结果:1 -> 7 -> 13 -> 6 -> 8 -> 2 -> 4 -> 3 -> 5 -> 9 -> 11 -> 10 -> 12
//(node v8.16.0)结果:1 -> 7 -> 13 -> 6 -> 8 -> 2 -> 4 -> 9 -> 11 -> 3 -> 10 -> 5 -> 12

图解:node12+版本下的执行顺序

在这里插入图片描述

  1. 首先是1层的同步任务直接执行:1、7、13
  2. 进入事件循环
  3. 执行1层的nextTick队列:6
  4. 执行1层的microTask队列:8
  5. 进入timer阶段,由于setTimeout1的回调任务先进入队列,因此先执行setTimeout1的2层同步任务:2、4
  6. 执行setTimeout1的2层nextTick队列:3
  7. 执行setTimeout1的2层microTask队列:5
  8. setTimeout1的2层代码均执行完毕,再执行setTimeout2的2层同步代码:9、11
  9. 执行setTimeout2的2层nextTick队列:10
  10. 执行setTimeout2的2层microTask队列:12

和浏览器中事件循环的区别

浏览器事件循环在每次宏任务执行后,浏览器有机会进行UI渲染,但实际渲染取决于是否触发了重排或重绘。
● 执行环境:浏览器的事件循环主要运行在JavaScript引擎和渲染引擎之间,而Node.js的事件循环是运行在单独的线程中。这意味着在浏览器中,事件循环可能与渲染进程共享同一个线程,可能会出现线程阻塞的情况。而在Node.js中,事件循环运行在单独的线程中,不会导致浏览器那样的渲染阻塞
● 宏任务和微任务的实现方式:在浏览器中,宏任务和微任务是通过HTML5规范中定义的消息队列来实现的。所有异步任务都被分为宏任务和微任务两种类型,并依次加入到对应的队列中。当当前的宏任务执行完毕后,会立即执行所有的微任务,然后再选择下一个宏任务执行。常见的宏任务包括setTimeout、setInterval、DOM事件等,常见的微任务包括Promise.then、MutationObserver等
● 微任务队列的执行时机:在浏览器事件循环中,每执行完一个宏任务后,便要检查执行微任务队列。而在Node事件循环中,微任务是在两个阶段之间执行的,即在"上一阶段"执行完,"下一阶段"开始前执行微任务队列中的任务。这意味着Node中的微任务是在两个阶段之间执行的,而浏览器中的微任务是在每个宏任务执行完后执行的
● 事件循环的执行机制:浏览器的事件循环是在HTML5中定义的规范,而Node的事件循环则是由libuv库实现。这两个环境的事件循环执行机制不相同,不可以混为一谈
● process.nextTick()的优先级:在Node.js中,process.nextTick()的优先级要高于其他微任务,也就是说,在两个阶段之间执行微任务时,若存在process.nextTick(),则先执行它,然后再执行其他微任务
● 事件循环的执行顺序:浏览器的事件循环机制包括同步代码的执行、宏任务队列的执行、微任务队列的执行以及浏览器UI线程的渲染工作。如果有Web Worker任务,也会被执行。而在Node.js中,事件循环的执行顺序包括脚本作为宏任务的执行、微任务的执行以及可能的Web Worker任务的执行

应用场景影响

● Node.js 更强调后端服务的高效I/O处理和高并发能力,因此其事件循环机制侧重于快速响应I/O事件和维持稳定的事件处理流。
● 浏览器 则侧重于UI渲染和用户交互的实时响应,故其事件循环设计确保了UI的流畅更新和事件的及时处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/703464.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IT行业的现状与未来发展趋势:重塑生活与工作的无限可能

随着科技的飞速发展&#xff0c;IT行业已经逐渐从幕后走到台前&#xff0c;成为推动全球经济和社会发展的核心动力。从云计算的普及、大数据的崛起&#xff0c;到人工智能的爆发&#xff0c;再到物联网、5G通信和区块链技术的日新月异&#xff0c;这些技术不仅正在深刻改变着我…

国网1376.1主站与采集终端通信协议和国网1376.2集中器本地通信模块接口协议报文解析工具

本文分享一个国网1376.1主站与采集终端通信协议的报文解析工具&#xff0c;同时本报文解析软件也支持国网1376.2集中器本地通信模块接口协议的报文解析。 下载链接: https://pan.baidu.com/s/1ngbBG-yL8ucRWLDflqzEnQ 提取码: y1de 主界面如下图所示&#xff1a; 同时本软件自…

企业微信推送报错40056

企业微信推送&#xff0c;报错 {"errCode":40056,"message":"weixin qy api errcode : 40056","result":99} 原因&#xff1a; 研究院的项目是新开的&#xff0c;虽然用的端华的配置信息&#xff0c;但应用没有授权&#xff0c;导致该…

Laravel中使用MinIO进行文件操作及ZIP解压

Laravel中使用MinIO进行文件操作及ZIP解压指南 介绍 在本指南中&#xff0c;我们将详细介绍如何在laravel框架中操作minio&#xff0c;包含方法有&#xff1a;桶列表&#xff0c;创建桶&#xff0c;修改桶&#xff0c;上传文件&#xff0c;删除文件&#xff0c;生成直传链接&…

揭秘SmartEDA魅力:为何众多学校青睐这款电路仿真软件?

在当今数字化、信息化的教育时代&#xff0c;电子电路仿真软件已成为电子学教学不可或缺的重要工具。其中&#xff0c;SmartEDA电路仿真软件以其强大的功能、用户友好的界面以及丰富的教育资源&#xff0c;赢得了众多学校的青睐。那么&#xff0c;究竟是什么原因让SmartEDA成为…

上海市嘉定区三德广场屋顶气膜篮球馆

上海市嘉定区三德广场屋顶气膜篮球馆为现代化城市综合体增添了一处先进、环保的运动设施。这座篮球馆不仅为广场周边居民提供了一个全天候的运动场所&#xff0c;也为嘉定区的体育爱好者带来了全新的健身体验。作为轻空间&#xff08;江苏&#xff09;膜科技有限公司&#xff0…

怎么扫码查看文件内容?多文件一键生成二维码的方法

现在日常生活中经常会看到很多的二维码中包含文件&#xff0c;扫码后在手机上预览文件内容或者下载文件&#xff0c;有很多的应用场景下被使用。通过扫描二维码的方式实现文件的传递&#xff0c;与传统方式相比更加方便快捷。 这种方式能够提升获取文件的便捷性&#xff0c;而…

【Unity-Timeline进度条显示与拖动】

利用Unity 自带的Timeline 可轻松实现场景的巡检漫游效果&#xff0c; 基本使用参考以下链接: Unity中的Timeline Unity学习笔记——TimeLine的简单使用方法&#xff08;一&#xff09; 这里主要介绍如何通过滑动条控制播放的进度&#xff0c;效果图附上。 话不多说&#xff…

【大华可见光摄像头】ffmpeg获取视频流并下载mp4 报错‘subtype‘ 不是内部或外部命令,也不是可运行的程序

我现在要通过ffmpeg获取大华摄像头视频流并下载成mp4&#xff0c;但我在cmd窗口运行下面命令的时候&#xff0c;发现报错&#xff1a; D:\Java\ffmpeg\ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe -y -i rtsp://admin:123xxx.xxx.xxx.xxx/cam/realmonitor?channel1&s…

【C++小语法技巧】命名空间和输入输出

在使用C语言编程过程中&#xff0c;C语言的要求之严格&#xff0c;编程过程之繁琐&#xff0c;大同小异的重复性工作&#xff0c;令C之父使用C语言编程时也深受其扰&#xff0c;于是乎C兼容C小语法诞生了 一、命名空间域&#xff08;解决C语言中命名冲突&#xff09; 1.定义命…

三无跨考,上岸热门211?

这个系列会邀请上岸学长学姐进行经验分享~ 今天分享经验的同学也是梦马班的学员&#xff0c;一战高分上岸福州大学&#xff01; 经验分享 一战零基础跨考福州大学866&#xff0c;初试395&#xff0c;信号141&#xff0c;最后本部录取排名前十。各位要报考福州大学的学弟学妹…

学习经验分享【36】论文投稿写作(非理工科文章)

业务进一步扩展&#xff0c;可辅导非理工科偏文科性质的论文辅导&#xff0c;有需要评职称但没有时间精力研究的或者其他相关需求的朋友可咨询了解。 人工智能技术在各领域的发展和思考&#xff0c;类似这种主题的文章。