多图生动详解浏览器与Node环境下的Event Loop(下)

简介: 接上文。

三、Node.js中的JavaScript


注: 此次讨论的都是针对Node.js 11.x以上的版本


本文分别讨论了JS在浏览器环境和Node.js环境这两种情况,那自然是有所区别的,后者相对于前者的过程分得更加细致


(1)node中的Event Loop


我们来看一张Node.js的 Event Loop 简图


915d1c6018b06a5d8ade8912a842357c.png


Node.js的Event Loop 是基于libuv实现的


通过 Node.js 的官方文档可以得知,其事件循环的顺序分为以下六个阶段,每个阶段都会处理专门的任务:


  • timers: 计时器阶段,用于处理setTimeout以及setInterval的回调函数


  • pending callbacks: 用于执行某些系统操作的回调,例如TCP错误


  • idle, prepare: Node内部使用,不用做过多的了解


  • poll: 轮询阶段,执行队列中的 I/O 队列,并检查定时器是否到时


  • check: 执行setImmediate的回调


  • close callbacks: 处理关闭的回调,例如 socket.destroy()


以上六个阶段,我们需要重点关注的只有四个,分别是 timerspollcheckclose callbacks


这四个阶段都有各自的宏队列,只有当本阶段的宏队列中的任务处理完以后,才会进入下一个阶段。在执行的过程中会不断检测微队列中是否存在待执行任务,若存在,则执行微队列中的任务,等到微队列为空了,再执行宏队列中的任务(这一点与浏览器非常类似,但在Node 11.x版本之前,并不是这样的运行机制,而是运行完当前阶段队列中的所有宏任务以后才会去检测微队列。对于11.x 之后的版本,虽然在官网我还没找到相关文字说明是这样的,但通过无数次的运行,暂且可以说是这样的,若各位找到相关的说明,可以留下评论)


同理,Node.js也有宏任务和微任务之分,我们来看一下常用的都有哪些


名称 举例(常用)
宏任务 setTimeout 、setInterval 、setImmediate
微任务 Promise 、process.nextTick


可以看到,在Node.js对比浏览器多了两个任务,分别是宏任务 setImmediate 和 微任务 process.nextTick


setImmediate 会在 check 阶段被处理


process.nextTick 是Node.js中一个特殊的微任务,因此会为它单独提供一个队列,称为 next tick queue,并且其优先级大于其它的微任务,即若同时存在 process.nextTickpromise,则会先执行前者


总结一下,Node.js在事件循环中涉及到了4个宏队列和2个微队列,如图所示


ec7a0cbae3f671f66a5f3845ef04b99e.png


在了解了基本过程以后,我们先来写一道简单的题


setTimeout(() => {
    console.log(1);
}, 0)
setImmediate(() => {
    console.log(2);
})
new Promise(resolve => {
    console.log(3);
    resolve()
    console.log(4);
})
.then(() => {
    console.log(5);
})
console.log(6);
process.nextTick(() => {
    console.log(7);
})
console.log(8);
/* 打印结果:
   3
   4
   6
   8
   7
   5
   1
   2
*/


首先毫无疑问,同步的代码一定是最先打印的,因此先打印的分别是 3 4 6 8


再来判断一下异步的代码,setTimeout 被送入 timers queuesetImmediate 被送入 check queuethen() 被送入 other microtask queueprocess.nextTick 被送入 next tick queue


然后我们按照上面图中的流程,首先检测到微队列中有待执行任务,并且我们说过,next tick queue 的优先级高于 other microtask queue,因此先打印了 7,然后打印了 5 ;到此为止微队列中的任务都被执行完了,接着就进入 timers queue 中阶段,所以打印了 1,当前阶段的队列为空了,按照顺序进入 poll 阶段,但发现队列为空,所以进入了 check 阶段,上面说过了这个阶段是专门处理 setImmediate 的,因此最后就打印了 2


(2)setTimeout和setImmediate


不知刚才讲了那么多,大家有没有发现,一个循环中,timers 阶段是先于 check 阶段的,那么是不是就意味着 setTimeout 就一定比 setImmediate 先执行呢?我们来看个例子


setTimeout(() => {
    console.log('setTimeout');
}, 0)
setImmediate(() => {
    console.log('setImmediate');
})


我们用node运行该段代码多次,发现得到了如下两种结果:


// 第一种结果
setTimeout
setImmediate
// 第二种结果
setImmediate
setTimeout


这是为什么呢?


这里我们给 setTimeout 设置的延迟时间是 0,表面上看上去好像是没有延迟,但其实运行起来延迟时间是大于0的


然后node开启一个事件循环是需要一定时间的。假设node开启事件循环需要2毫秒,然后 setTimeout 实际运行的延迟时间是10毫秒,即事件循环开始得比 setTimeout 早,那么在第一轮事件循环运行到 timers 时,发现并没有 setTimeout 的回调需要执行,因此就进入了下一阶段,尽管此时 setTimeout 的延迟时间到了,但它只能在下一轮循环时被执行了,所以本次事件循环就先打印了 setImmediate,然后在下一次循环时打印了 setTimeout


这就是刚才第二种结果出现的原因


那么为何存在第一种情况也就更好理解了,那就是 setTimeout 的实际的延迟事件小于node事件循环的开启事件,所以能在第一轮循环中被执行


了解了为何出现上述原因以后,这里提出两个问题:


  1. 如何能做到一定先打印 setTimeout ,后打印 setImmediate


  1. 如何能做到一定先打印 setImmediate ,后打印 setTimeout


这里我们来分别实现一下这两个需求


实现一:


既然要让 setTimeout 先打印,那么就让它在第一轮循环时就被执行,那么我们只需要让事件循环开启的事件晚一点就好了。所以可以写一段同步的代码,让同步的代码执行事件长一点,然后就可以保证在进入 timers 阶段时,setTimeout 的回调已被送入 timers queue


setTimeout(() => {
    console.log('setTimeout');
}, 0)
setImmediate(() => {
    console.log('setImmediate');
})
let start = Date.now()
// 让同步的代码运行30毫秒
while(Date.now() - start < 30)


多次运行代码发现,每次都是先打印了 setTimeout,然后才打印的 setImmediate


实现二:


既然要让 setTimeout 后打印,那么就要想办法让它在第二轮循环时被执行,那么我们可以让 setTimeout 在第一轮事件循环跳过 timers 阶段后执行


刚开始我们讲过,poll 阶段是为了处理各种 I/O 事件的,例如文件的读取就属于 I/O 事件,所以我们可以把 setTimeoutsetImmediate 的代码放在一个文件读取操作的回调内,这样在第一轮循环到达 poll 阶段时,会将 setTimeout 送入 timers queue,但此时早已跳过了 timers 阶段,所以其只会在下一轮循环时被打印 ;同时 setImmediate 此时被送入了 check queue ,那么在离开 poll 阶段以后就可以顺利得先打印 setImmediate


const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
});


多次运行代码发现,每次都是先打印了 setImmediate,然后才打印的 setTimeout


四、结束语


一篇完整的Event Loop就讲到这里了,作者也是花了两天的时间,才将其搞懂,并且整理成博客,希望这篇文章对大家能有所帮助吧,哈哈最主要的是,在面试中不要像作者一样再在这个上面栽跟头了


相关文章
|
21天前
如何在不同的浏览器环境中确保时间戳转换的兼容性?
通过以上这些措施,可以在不同的浏览器环境中提高时间戳转换的兼容性,确保应用能够在各种浏览器中稳定运行,为用户提供良好的体验。
|
11天前
|
机器学习/深度学习 JavaScript Cloud Native
Node.js作为一种快速、可扩展的服务器端运行时环境
Node.js作为一种快速、可扩展的服务器端运行时环境
24 8
|
22天前
|
JavaScript 前端开发 数据处理
模板字符串和普通字符串在浏览器和 Node.js 中的性能表现是否一致?
综上所述,模板字符串和普通字符串在浏览器和 Node.js 中的性能表现既有相似之处,也有不同之处。在实际应用中,需要根据具体的场景和性能需求来选择使用哪种字符串处理方式,以达到最佳的性能和开发效率。
|
1月前
|
存储 JavaScript 网络协议
浏览器与 Node 的事件循环
浏览器和Node.js的事件循环是异步操作的核心机制。它们通过管理任务队列和回调函数,确保程序在处理耗时任务时不会阻塞主线程,从而实现高效、响应式的应用开发。
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
6月前
|
前端开发
windows10 安装node npm 等前端环境 并配置国内源
windows10 安装node npm 等前端环境 并配置国内源
346 3
|
3月前
|
SQL JavaScript 数据库
sqlite在Windows环境下安装、使用、node.js连接
sqlite在Windows环境下安装、使用、node.js连接
|
3月前
|
JavaScript 前端开发 Windows
NodeJS的环境部署
介绍如何在Windows操作系统上安装Node.js环境,包括下载长期支持版本的Node.js、安装程序、编写测试代码并执行,以及如何在WebStorm集成开发环境中配置和运行Node.js。
50 1
|
4月前
|
编解码 JavaScript 前端开发
JS逆向浏览器脱环境专题:事件学习和编写、DOM和BOM结构、指纹验证排查、代理自吐环境通杀环境检测、脱环境框架、脱环境插件解决
JS逆向浏览器脱环境专题:事件学习和编写、DOM和BOM结构、指纹验证排查、代理自吐环境通杀环境检测、脱环境框架、脱环境插件解决
128 1
|
4月前
|
缓存 JavaScript Ubuntu
Node.js环境怎么搭建?
【8月更文挑战第4天】Node.js环境怎么搭建?
72 1