JavaScript事件循环

大厂面试题分享 面试题库
后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
一、异步执行原理
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 在处理宏任务和微任务的逻辑时的执行情况如下:
JavaScript 引擎首先从宏任务队列中取出第一个任务
执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行,如果在这一步过程中产生新的微任务,也需要执行(在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行)
然后再从宏任务队列中取下一个,执行完毕后,再次将 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"复制代码
那这段代码执行过程是怎么的呢?
遇到第一个console,它是同步代码,加入执行栈,执行并出栈,打印 "同步代码1"
遇到setTimeout,它是一个宏任务,加入宏任务队列
遇到new Promise 中的console,为同步代码,加入执行栈,执行并出栈,打印 "同步代码2"
遇到Promise then,它是一个微任务,加入微任务队列
遇到第三个console,它是同步代码,加入执行栈,执行并出栈,打印 "同步代码3"
此时执行栈为空,去执行微任务队列中所有任务,打印 "promise.then"
执行完微任务队列中的任务,就去执行宏任务队列中的一个任务,打印 "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的运行机制如下:
V8引擎负责解析JavaScript脚本;
解析后的代码,调用Node API;
libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎;
V8引擎将结果返回给用户;
2. 事件循环的流程
其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。下面是Eventloop 事件循环的流程:

整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。下面来看下这六个阶段都做了哪些事:
timers 阶段:执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制;
I/O callbacks 阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调;
idle, prepare 阶段:仅Node.js内部使用,可以忽略;
poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等;
check 阶段:执行 setImmediate() 的回调;
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 callbacks和poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为timeout、setImmediate。
四、Node与浏览器事件循环的差异
Node.js与浏览器的 Event Loop 差异如下:
Node.js:microtask 在事件循环的各个阶段之间执行;
浏览器:microtask 在事件循环的 macrotask 执行完之后执行;

Nodejs和浏览器的事件循环流程对比如下:
执行全局的 script 代码(与浏览器无差);
把微任务队列清空:注意,Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务;
开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到系统限制);
步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环。
大厂面试题分享 面试题库
后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
相关文章:

JavaScript事件循环
大厂面试题分享 面试题库后端面试题库 (面试必备) 推荐:★★★★★地址:前端面试题库一、异步执行原理1. 单线程的JavaScript我们知道,JavaScript是一种单线程语言,它主要用来与用户互动,以及操…...

华为OD机试真题Python实现【最少停车数】真题+解题思路+代码(20222023)
最少停车数 题目 特定大小的停车场 数组cars表示 其中1表示有车0表示没车 车辆大小不一,小车占一个车位(长度1) 货车占两个车位(长度2) 卡车占三个车位(长度3) 统计停车场最少可以停多少辆车 返回具体的数目 🔥🔥🔥🔥🔥👉👉👉👉👉👉 华为OD机试(Pyt…...

Python每日一练(20230223)
目录 1. 合并区间 2. 单词接龙 3. N皇后 附录:回溯算法 基本思想 一般步骤 1. 合并区间 难度:★★ 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间,并返回…...

Flask----------第一个flask项目,debug、host、port的配置
目录 1.flask 1.简介 2.flask框架的优势 2.第一个flask项目 3.debug 开启debug方法 1.专业版 2.社区版 4.修改host 5. 修改port端口 1.flask 1.简介 Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeug本质是So…...

容器技术概述
容器技术概述 软件应用程序通常依赖于运行时环境提供的其他库、配置文件或服务。软件应用程序的传统运行环境是物理主机或虚拟机,应用程序依赖项作为主机的一部分安装。 例如,考虑一个 Python 应用程序,它需要访问实现 TLS 协议的公共共享库…...

「SAP」ABAP模块学习需要了解什么?快收下这份ABAP技术栈指南【附技能树】
💂作者简介: THUNDER王,一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计专业大二本科在读,阿里云社区专家博主,华为云社区云享专家,CSDN SAP应用技术领域新兴创作者。 在学习工…...

【python 基础篇 九】python的常用数据类型操作-------时间日历
目录1.python时间操作1.1 time模块1.2 calendar模块1.3 datetime模块1.python时间操作 python程序能用很多方式处理日期和时间,转换日期格式也是一个常见功能。 1.1 time模块 提供了处理时间和表示之间转换的功能 获取当前时间戳 概念:从0时区的1…...

华为OD机试真题Python实现【相同字符连续出现的最大次数】真题+解题思路+代码(20222023)
相同字符连续出现的最大次数 题目 输入一串字符串 字符串长度不超过100 查找字符串中相同字符连续出现的最大次数 🔥🔥🔥🔥🔥👉👉👉👉👉👉 华为OD机试(Python)真题目录汇总 ## 输入 输入只有一行,包含一个长度不超过100的字符串 输出描述 输出只…...

【Unity3D】空间和变换
1 空间 1.1 左右手坐标系及其法则 1.1.1 左右手坐标系 左手坐标系与右手坐标系Unity 局部空间、世界空间、裁剪空间、屏幕空间都采用左手坐标系,只有观察空间采用右手坐标系。 左右手坐标系除了坐标系朝向(旋向性)不同,还存在以…...

脑洞|ChatGPT加持下,ChatOps将如何革新团队协作与运维管理?
要说近期科技圈 “顶流”,非 ChatGPT 莫属。 比起目前常见的语音助手与聊天 bot,这位机器人显得更有 “人味儿”,不仅能模拟人类的语气,跟你聊得有来有回,还能写剧本、编音乐、写代码。 说到聊天工具,就让…...

华为OD机试真题Python实现【找数字】真题+解题思路+代码(20222023)
找数字 题目 给一个二维数组nums,对于每一个元素num[i],找出距离最近的且值相等的元素,输出横纵坐标差值的绝对值之和,如果没有等值元素,则输出-1。 例如: 输入数组nums为 0 3 5 4 2 2 5 7 8 3 2 5 4 2 4对于 num[0][0] = 0,不存在相等的值。 对于 num[0][1] = 3,存…...

【Database-01】达梦数据库Docker版下载安装
1、前往达梦数据库官网下载 https://www.dameng.com/1.1、选择数据库 - 数据库产品系 1.2、选择 达梦数据库管理系统(DM8) 1.3、点击试用下载 1.4、注册达梦账户 1.5、选择DM8 Docker镜像 https://www.dameng.com/list_103.html1.6、或者使用以下网址也…...

Allegro如何打开格点显示效果操作指导
Allegro如何打开格点显示效果操作指导 Allegro可以设置格点显示效果,以格点来判定走线等等是否都处于格点上,如下图 如何打开格点显示效果,具体操作如下 点击Setup点击Grids...

电子技术——反馈放大器的分析方法总结
电子技术——反馈放大器的分析方法总结 第一种也是最简单的估算方法,直接拿出反馈网络,计算 β\betaβ 则假设在 AβA\betaAβ 无限大的情况下有 Af≃1/βA_f \simeq 1/\betaAf≃1/β 。开环法。比第一种方法更能精确的估计 AAA 和 β\betaβ 的值。系…...

微服务系统启动,环境从0开始的搭建过程
1. JDK的下载安装(傻瓜式) 安装过程傻瓜式,直接一步到位。我安装的版本为:jdk-17_windows-x64_bin 2. 集成开发工具的下载安装:IDEA(傻瓜式) ideaIU-2021.2.1 网上资源很多,自己找…...

手工测试1年经验面试,张口要13K,我真是服了····
由于朋友临时有事, 所以今天我代替朋友进行一次面试,他需要应聘一个测试工程师, 我以很认真负责的态度完成这个过程, 大概近30分钟。 主要是技术面试, 在近30分钟内, 我与被面试者是以交流学习的方式进行的…...

【保姆级】手把手捋动态代理流程(JDK+Cglib超详细源码分析)
简介动态代理,通俗点说就是:无需声明式的创建java代理类,而是在运行过程中生成"虚拟"的代理类,被ClassLoader加载。 从而避免了静态代理那样需要声明大量的代理类。上面的简介中提到了两个关键的名词:“静态…...

Appium自动化测试 Inspector定位Webview/H5页面元素
目录操作步骤Python操作该混合App代码Appium在操作混合App或Android App的H5页面时, 常常需要定位H5页面中的元素, 传统方式是 FQ 使用Chrome://inspect来定位元素, 环境准备相当繁琐, 不仅需要想办法FQ, 而且还需要Android设备安装Google框架以及手机版Chrome浏览器以及相应的…...

数组求和方法总结,学点干货
1.循环 (新手用) 1.1 普通for 循环 简单质朴 const arr [1, 2, 3, 4, 5];let sum 0;for (let i 0; i < arr.length; i) {sum arr[i];}1.2 for in 循环 与普通for循环大同小异 const arr [1, 2, 3, 4, 5];let sum 0;for (let i in arr) {sum …...

斗地主洗牌发牌-课后程序(JAVA基础案例教程-黑马程序员编著-第六章-课后作业)
【案例6-4】 斗地主洗牌发牌 【案例介绍】 1.任务描述 扑克牌游戏“斗地主”,相信许多人都会玩,本案例要求编写一个斗地主的洗牌发牌程序,要求按照斗地主的规则完成洗牌发牌的过程。一副扑克总共有54张牌,牌面由花色和数字组成…...

基于antd封装的二次业务筛选组件-table-filter
文档地址:https://flowerofsummer.github.io/components/ 业务筛选组件 支持各种类型的高级搜索组件 基础用法 组件响应式布局,默认显示两行,可以通过 maxLineCount 配置最多显示行数每行个数: 如果含有 time-range࿰…...

逆向-还原代码之max 再画堆栈图 (Interl 64)
// source code #include <stdio.h> void max(int * a, int * b) { if (*a < *b) *a *b; } int main() { int a 5, b 6; max(&a, &b); printf("a, b max %d\n", a); return 0; } // 再画堆栈图 下周一(2.27…...

GitHub标星30K+的Java面试八股文长啥样?
2023年的互联网行业竞争越来越严峻,面试也是越来越难,一直以来我都想整理一套完美的面试宝典,奈何难抽出时间,这套1000道的Java面试手册我整理了整整1个月,上传到Git上目前star数达到了30K 一、32 道 MySQL 面试题 1&…...

CVE-2022-39197 POC(CobaltStrike XSS <=4.7)漏洞复现
漏洞说明 根据9.20日CobaltStrike官方发布的最新4.7.1版本的更新日志中介绍,<4.7的teamserver版本存在XSS漏洞,从而可以造成RCE远程代码执行 一位名为“Beichendream”的独立研究人员联系我们,告知我们他们在团队服务器中发现的一个 XSS …...

我们来说说蹿红的AIGC到底是什么?ChatGPT又是什么?
近期,人工智能(AI)领域动作频频,OPENAI公司Chat GPT的出现,标志着人工智能的研究与应用已经进入了一个崭新的发展阶段,国内腾讯、阿里巴巴、百度、易网、国外微软、谷歌、苹果、IBM、Amazon,等互…...

新手如何从零开始搭建配置Windows云服务器?
新手如何从零开始搭建配置Windows云服务器?本文是搭建 Windows 云服务器入门教程,主要介绍如何从零开始,以最简单的方式搭建和配置你的Windows 云服务器。如果您之前没有搭建云服务器的经验,建议您按照本文介绍的方式来购买和配置…...

百趣代谢组学-抑郁症居然“男女有别”,脑膜淋巴管起关键作用!
文章标题:A functional role of meningeal lymphatics in sex difference of stress susceptibility in mice 发表期刊:Nature Communications 影响因子:17.694 发表时间:2022年8月 作者单位:中山大学中山医学院 …...

C语言实现用堆解决 TOP-K 问题
目录 TopK函数实现 如何测试 完整源码 生活中我们经常能见到TopK问题,例如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 所以,TopK问题即求出一组数据中前K个最大或最小的元素,一般情况下,数据量都…...

MySQL 数据库基础命令
MySQL 基础命令 一.了解数据库 1、了解数据库对象 1.表: 用于以有组织方式存储数据。以行和列的格式包含数据。 2.索引: 是内部表结构,MySQL 用它基于一列或多列的值来提供对表中各行的快速访问。 3.视图: 是虚拟表&#…...

说一下this,实现apply、call
理解this 在ES5中,this的指向始终坚持一个原理:“this永远指向最后调用它的那个对象”,切记这句话。下面看几个例子。 例一 var obj {name: zhangsan,say: function() {console.log(this.name);} }obj.say() // zhangsan 最基本的使用&am…...