多图生动详解浏览器与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就讲到这里了,作者也是花了两天的时间,才将其搞懂,并且整理成博客,希望这篇文章对大家能有所帮助吧,哈哈最主要的是,在面试中不要像作者一样再在这个上面栽跟头了


相关文章
|
3月前
|
JavaScript 开发者
什么是浏览器环境下事件的 Propagation
什么是浏览器环境下事件的 Propagation
49 1
|
4月前
|
JavaScript 前端开发 算法
Node.js中的process.nextTick与浏览器环境中的nextTick有何不同?
Node.js中的process.nextTick与浏览器环境中的nextTick有何不同?
|
6月前
|
开发框架 NoSQL JavaScript
mongoDB入门教程四:安装Node+express环境支撑
mongoDB入门教程四:安装Node+express环境支撑
53 0
|
6月前
|
JSON JavaScript 前端开发
基于promise用于浏览器和node.js的http客户端的axios
基于promise用于浏览器和node.js的http客户端的axios
38 0
|
1月前
|
开发框架 JavaScript 中间件
node+express搭建服务器环境
node+express搭建服务器环境
node+express搭建服务器环境
|
1天前
|
数据可视化 JavaScript NoSQL
搭建接口平台YApi详解(含搭建node环境)
搭建接口平台YApi详解(含搭建node环境)
8 0
|
9天前
|
JSON JavaScript 关系型数据库
❤Nodejs 第十六章(Nodejs环境安装和依赖使用)
【4月更文挑战第16天】本文介绍了Node.js环境安装和项目搭建步骤。检查Node.js和npm版本安装核心依赖,如Express(Web应用框架)、MySQL库、CORS(解决跨域问题)、body-parser(解析POST数据)、express-jwt和jsonwebtoken(JWT验证)、multer(文件上传处理)、ws(WebSocket支持),以及可选的dotenv(管理环境变量)和ejs(模板引擎)。完整源码可在Gitee开源项目[nexusapi](https://gitee.com/lintaibai/nexusapi)中找到。
21 0
|
10天前
|
运维 JavaScript Java
Serverless 应用引擎产品使用之阿里云Serverless函数计算中,在Node.js环境中执行jar文件如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
19 0
|
3月前
|
前端开发 JavaScript UED
浏览器环境下事件对象 stopPropagation 方法的含义和使用场景介绍
浏览器环境下事件对象 stopPropagation 方法的含义和使用场景介绍
31 1
|
3月前
|
JavaScript 开发者
什么是浏览器环境下的 Event Propagation(事件传播)
什么是浏览器环境下的 Event Propagation(事件传播)
18 1