JavaScript事件循环

news2025/1/15 19:39:48

大厂面试题分享 面试题库

后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库

一、异步执行原理


1. 单线程的JavaScript

我们知道,JavaScript是一种单线程语言,它主要用来与用户互动,以及操作DOM。

JavaScript 有同步和异步的概念,这就解决了代码阻塞的问题:

  • 同步:如果在一个函数返回的时候,调用者就能够得到预期结果,那么这个函数就是同步的

  • 异步:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。

那单线程有什么影响呢?

  • 在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。

  • 得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间的好处。

2. 多线程的浏览器

JS 是单线程的,在同一个时间只能做一件事情,那为什么浏览器可以同时执行异步任务呢?

这是因为浏览器是多线程的,当 JS 需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。也就是说,JavaScript是单线程指的是执行JavaScript代码的线程只有一个,是浏览器提供的JavaScript引擎线程(主线程)。除此之外,浏览器中还有定时器线程、 HTTP 请求线程等线程,这些线程主要不是来执行 JS 代码的。

比如主线程中需要发送数据请求,就会把这个任务交给异步 HTTP 请求线程去执行,等请求数据返回之后,再将 callback 里需要执行的 JS 回调交给 JS 引擎线程去执行。也就是说,浏览器才是真正执行发送请求这个任务的角色,而 JS 只是负责执行最后的回调处理。所以这里的异步不是 JS 自身实现的,而是浏览器为其提供的能力。

下图是Chrome浏览器的架构图:

可以看到,Chrome不仅拥有多个进程,还有多个线程。以渲染进程为例,就包含GUI渲染线程、JS引擎线程、事件触发线程、定时器触发线程、异步HTTP请求线程。这些线程为 JS 在浏览器中完成异步任务提供了基础。

二、浏览器的事件循环


JavaScript的任务分为同步异步

  • 同步任务: 在主线程上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,

  • 异步任务: 不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。

上面提到了任务队列和执行栈,下面就先来看看这两个概念:

1. 执行栈与任务队列

1)执行栈:它是一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。以下图为例:

当执行这段代码时,首先会执行一个 main 函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。

JavaScript在按顺序执行执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出此方法,然后继续执行下一个方法。

2)任务队列: 它使用到的是数据结构中的队列结构,用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。

JavaScript在执行代码时,会将同步的代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。

JavaScript任务的执行顺序如下:

在事件驱动的模式下,至少包含了一个执行循环来检测任务队列中是否有新任务。通过不断循环,去取出异步任务的回调来执行,这个过程就是事件循环,每一次循环就是一个事件周期。

2. 宏任务和微任务

任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列宏任务(macro task)队列。常见的任务如下:

  • 宏任务: script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

  • 微任务: Promise、MutaionObserver、process.nextTick(Node.js 环境);

任务队列执行顺序如下:

可以看到,Eventloop 在处理宏任务和微任务的逻辑时的执行情况如下:

  1. JavaScript 引擎首先从宏任务队列中取出第一个任务

  1. 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行,如果在这一步过程中产生新的微任务,也需要执行(在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行)

  1. 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。

也是就是说,一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。

下面通过一个例子来体会事件循环:

console.log('同步代码1')

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

newPromise((resolve) => {
  console.log('同步代码2')
  resolve()
}).then(() => {
    console.log('promise.then')
})

console.log('同步代码3')
复制代码

代码输出结果如下:

"同步代码1""同步代码2""同步代码3""promise.then""setTimeout"复制代码

那这段代码执行过程是怎么的呢?

  1. 遇到第一个console,它是同步代码,加入执行栈,执行并出栈,打印 "同步代码1"

  1. 遇到setTimeout,它是一个宏任务,加入宏任务队列

  1. 遇到new Promise 中的console,为同步代码,加入执行栈,执行并出栈,打印 "同步代码2"

  1. 遇到Promise then,它是一个微任务,加入微任务队列

  1. 遇到第三个console,它是同步代码,加入执行栈,执行并出栈,打印 "同步代码3"

  1. 此时执行栈为空,去执行微任务队列中所有任务,打印 "promise.then"

  1. 执行完微任务队列中的任务,就去执行宏任务队列中的一个任务,打印 "setTimeout"

从上面的宏任务和微任务的工作流程中,可以得出以下结论:

  • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。

  • 微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10 个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10 个微任务让宏任务的执行时间延长了 100ms。

  • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行(优先级更高)。

那么问题来了,为什么要将任务队列分为微任务和宏任务呢,他们之间的本质区别是什么呢?

JavaScript在遇到异步任务时,会将此任务交给其他线程来执行(比如遇到setTimeout任务,会交给定时器触发线程去执行,待计时结束,就会将定时器回调任务放入任务队列等待主线程来取出执行),主线程会继续执行后面的同步任务。

对于微任务,比如promise.then,当执行promise.then时,浏览器引擎不会将异步任务交给其他浏览器的线程去执行,而是将任务回调存在一个队列中,当执行栈中的任务执行完之后,就去执行promise.then所在的微任务队列。

所以,宏任务和微任务的本质区别如下:

  • 微任务:不需要特定的异步线程去执行,没有明确的异步任务去执行,只有回调

  • 宏任务:需要特定的异步线程去执行,有明确的异步任务去执行,有回调

三、Node.js的事件循环


1. 事件循环的概念

对于Node.js的事件循环,官网的描述:当Node.js启动时,它会初始化一个事件循环,来处理输入的脚本,这个脚本可能进行异步API的调用、调度计时器或调用process.nextTick(),然后开始处理事件循环。

JavaScript和Node.js是基于V8 引擎的,浏览器中包含的异步方式在 NodeJS 中也是一样的。除此之外,Node.js中还有一些其他的异步形式:

  • 文件 I/O:异步加载本地文件。

  • setImmediate():与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行。

  • process.nextTick():在某些同步任务完成后立马执行。

  • server.close、socket.on('close',...)等:关闭回调。

这些异步任务的执行就需要依靠Node.js的事件循环机制了。

Node.js 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js使用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现的,如下图所示:

根据上图,可以看到Node.js的运行机制如下:

  1. V8引擎负责解析JavaScript脚本;

  1. 解析后的代码,调用Node API;

  1. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎;

  1. V8引擎将结果返回给用户;

2. 事件循环的流程

其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。下面是Eventloop 事件循环的流程:

整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。下面来看下这六个阶段都做了哪些事:

  1. timers 阶段:执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制;

  1. I/O callbacks 阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调;

  1. idle, prepare 阶段:仅Node.js内部使用,可以忽略;

  1. poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等;

  1. check 阶段:执行 setImmediate() 的回调;

  1. close callbacks 阶段:执行关闭请求的回调函数,比如socket.on('close', ...)

注意:上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段,这里也是与浏览器中逻辑差异较大的地方。

其中,这里面比较重要的就是第四阶段:poll,这一阶段中,系统主要做两件事:

  • 回到 timer 阶段执行回调

  • 执行 I/O 回调

在进入该阶段时如果没有设定了 timer 的话,会出现以下情况:

(1)如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制;

(2)如果 poll 队列为空时,会出现以下情况:

  • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调;

  • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去;

当设定了 timer 且 poll 队列为空,则会判断是否有 timer 超时,如果有的就会回到 timer 阶段执行回调。

这一过程的具体执行流程如下图所示:

3. 宏任务和微任务

Node.js事件循环的异步队列也分为两种:宏任务队列和微任务队列。

  • 常见的宏任务:setTimeout、setInterval、setImmediate、script(整体代码)、 I/O 操作

  • 常见的微任务:process.nextTick、new Promise().then(回调)

4. process.nextTick()

上面提到了process.nextTick(),它是node中新引入的一个任务队列,它会在上述各个阶段结束时,在进入下一个阶段之前立即执行。

举个例子:

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

Promise.resolve().then(() => {
    console.error('promise')
})

process.nextTick(() => {
    console.error('nextTick')
})
复制代码

输出结果如下:

nextTick
promise
timeout
复制代码

可以看到,process.nextTick()是优先于promise的回调执行。

5. setImmediate 和 setTimeout

上面还提到了setImmediate 和 setTimeout,这两者很相似,主要区别在于调用时机的不同:

  • setImmediate:在poll阶段完成时执行,即check阶段;

  • setTimeout:在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行;

例如下面的代码:

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

setImmediate(() => {
  console.log('setImmediate')
})
复制代码

输出结果如下:

timeout
setImmediate
复制代码

在上面代码的执行过程中,第一轮循环后,分别将 setTimeout 和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入timers 阶段,执行定时器队列回调,然后 pending callbackspoll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为timeout、setImmediate。

四、Node与浏览器事件循环的差异


Node.js与浏览器的 Event Loop 差异如下:

  • Node.js:microtask 在事件循环的各个阶段之间执行;

  • 浏览器:microtask 在事件循环的 macrotask 执行完之后执行;

Nodejs和浏览器的事件循环流程对比如下:

  1. 执行全局的 script 代码(与浏览器无差);

  1. 把微任务队列清空:注意,Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务

  1. 开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到系统限制);

  1. 步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环。

大厂面试题分享 面试题库

后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

50-Jenkins-Lockable Resources插件实现资源锁定

Lockable Resources插件实现资源锁定前言安装插件使用插件资源配置Pipeline中使用前言 用来阻止多个构建在同一时间试图使用同一个资源。这里的资源可能是一个节点、一个代理节点、一组节点或代理节点的集合,或者仅仅是一个用于上锁的名字。如果指定的资源没有在全…

ASP.NET MVC | 创建应用程序

目录 首先 NO.1 No.2 App_Data 文件夹 Content 文件夹 Controllers 文件夹 Models 文件夹 Views 文件夹 Scripts 文件夹 最后 首先 一步一步的来,电脑上需要安装vs2019软件,版本高低无所谓,就是功能多少而已。 长这样的&#xff0…

无公网IP如何外网异地登录访问电商进销存系统?

电商进销存系统软件是电商企业必备的重要软件之一。 集订单管理、货品管理、采购管理等功能于一体,主要帮助广大电商用户实现准确、高效的订单处理及精细化的仓储管理。 电商进销存系统软件一般采用B/S结构,用户可在异地访问系统、查看货品库存及管理订…

第五期(2022-2023)传统行业云原生技术落地调研——金融篇 现已开启

随着数字化浪潮的来临,云原生技术正在改变着各行各业,通过IT变革驱动业务创新发展,促进企业自身以及产业生态的转型升级。 因此,灵雀云联合云原生技术实践联盟(CNBPA)和行业内头部厂商F5,共同发…

小黑子的线性代数:第一章

线代从入门到入土:一小黑子的线代系列:第一章1. 行列式1.1 二阶行列式1.2 三阶行列式1.3 小结2. 全排列与逆序数2.1 全排列2.2 逆序数3. 对换4. n阶行列式的定义5. 余子式和代数余子式6. 行列式的性质6.1 转置行列式6.2 对换变号6.3 提取公因子6.4 行列式…

华为OD机试C++实现 - 最小步骤数

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

GEE:样本点选择教程

本文记录了在GEE平台上标记样本的技巧和代码脚本,样本点可以用来做土地利用分类、植被提取、水藻提取、冰川提取、农作物提取等应用中。可以应用到的方法包括随机森林(RF)分类,支持矢量机(SVM)分类&#xf…

JavaSE之常用关键字学习

文章目录Java常用关键字学习1、static关键字学习1.1 用法一:修饰成员变量1.2 用法二:修饰成员方法1.3 用法三:修饰代码块1.4 用法四:修饰内部类类1.5 单例设计模式2、extends关键字学习2.1 继承的特点2.2 方法重写3、this、super关…

nvm基础命令

nvm基础命令 有了nvm之后就可以进行node下载了。下面举一个简单的例子: nvm version: 查看nvm版本 nvm list:查看本地拥有的node版本 nvm install xxx:安装版本号为xxx的node nvm use xxx:将node版本切换为xxx 以…

微信接口wx.login()、wx.request()中获取的内容不能赋值给全局变量(已解决)

小程序问题总结01 微信接口wx.login()、wx.request()中获取的内容不能赋值给全局变量(已解决) 在写登录模块的时候,需要使用微信的wx.login()接口获取临时code,并利用临时code向开发者服务器端发送请求,然后获取open…

自动化测试优势和劣势

一、自动化测试概述 软件自动化测试是相对手工测试而存在的,由测试人员根据测试用例中描述的规程一步步执行测试,得到实际结果与期望结果的比较。在此过程中,节省人力、时间或硬件资源,提高测试效率。 二、自动化测试优势&劣…

和日期相关的代码和bug——一道力扣题中的小发现

目录 Day of the Week 题目大意 常规方法 Python代码 Golang代码 C代码 基姆拉尔森公式 Python代码 Golang代码 C代码 使用库函数 Python代码 Golang代码 C代码 Day of the Week Given a date, return the corresponding day of the week for that date. The inp…

Photon Vectorized Engine 学习记录

Photon Hash Aggregation Vectorization Photon Hash Join 的向量化的要点是:使用开放地址法。步骤: 向量化计算 hash 值基于 hash 向量化计算 bucket 下标,得到 bucket index 向量基于 bucket index 向量中记录的下标找到 bucket&#xff…

领导催我优化SQL语句,我求助了ChatGPT。这是ChatGPT给出的建议,你们觉得靠谱吗

作为一个程序员,无论在面试还是工作中,优化SQL都是绕不过去的难题。 为啥?工作之后才会明白,随着公司的业务量增多,SQL的执行效率对程系统运行效率的影响逐渐增大,相对于改造代码,优化SQL语句是…

线上插画培训班有用吗,教你选靠谱的插画课程

线上插画培训班有用吗,教你选靠谱的插画课程,推荐5个靠谱的动漫插画培训课程,各有特色和优势,相信可以给大家一些参考! 一:5个靠谱的动漫插画网课 1、轻微课(五颗星) 主打课程有日…

机器学习:基于逻辑回归对某银行客户违约预测分析

机器学习:基于逻辑回归对某银行客户违约预测分析 文章目录机器学习:基于逻辑回归对某银行客户违约预测分析一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤1.逻辑回归2.业务理解3.读取数据4.数据理解5.数据准备6.逻辑回归模型训练7.模型评…

【第41天】实现一个简单选择排序

本文已收录于专栏🌸《Java入门一百例》🌸学习指引序、专栏前言一、选择排序二、【例题1】1.题目描述2、解题思路3、模板代码三、推荐专栏序、专栏前言 本专栏开启,目的在于帮助大家更好的掌握学习Java,特别是一些Java学习者难以在…

「epoll」深入linux内核中是如何实现多路的IO管理的

进程在 Linux 上是一个开销不小的家伙,先不说创建,光是上下文切换一次就得几个微秒。所以为了高效地对海量用户提供服务,必须要让一个进程能同时处理很多个 tcp 连接才行。现在假设一个进程保持了 10000 条连接,那么如何发现哪条连…

实战:手把手教你colossal-AI复现Chatgpt的流程

相信很多人都看了使用colossal-AI复现Chatgpt的流程的文章,但实际上看过了,不免有人发出“说得贼明白,就是自己做不出来”的感叹吧。本人公开一下实战过程,给有兴趣复现chatgpt流程的朋友一个参考。 一、环境搭建: 1…

Redis四 原理篇

《Redis四 原理篇》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《Redis四 原理篇》《Redis四 原理篇》1、原理篇-Redis数据结构1.1 Redis数据结构-动态字符串1.2 Redis数据结构-intset1.3 Redis数据结构-Dict1.4 Redis数据结构-ZipList1.4.1 Redis数据…