理解JS的事件循环机制(Event Loop)

news2024/11/26 23:24:44

文章目录

  • 一、前言
  • 二、首先理解
  • 三、灵魂三问
    • 1. JS为什么是单线程的?
    • 2. 为什么需要异步? (为什么要有事件循环机制?)
    • 3. 单线程又是如何实现异步的呢?
  • 四、什么是事件循环?
  • 五、事件循环(Event Loop )执行顺序
  • 六、事件循环实例讲解
  • 七、关于setTimeout
  • 八、思考问题
    • 1. JS 中的计时器能做到精确计时吗?为什么?
    • 2. 为什么script(整体代码)是宏任务却优先执行?


一、前言

JS是单线程语言,但是又可以做到异步处理高并发请求,这时就用到了JavaScript的事件循环机制。
理解事件循环,可以帮助我们准确分析和运用各种异步形式,减少代码的不确定性,在一些执行效率优化上也能有明确的思路。

二、首先理解

  • JS是单线程语言
  • 分为 同步任务异步任务
  • 同步任务:立即执行的任务;在主线程上排队执行,形成一个执行栈;只有前一个任务执行完毕,才能继续执行下一个任务。
  • 异步任务:不进入主线程,而进入“任务队列”的任务;只有等主线程任务全部执行完毕。“任务队列”的任务才会进入主线程执行。
  • 任务队列分为 微任务队列宏任务队列
  • 微任务:较短时间内可以完成的任务
  • 宏任务:需要相对较长时间才能完成的任务
微任务(microtask)宏任务(macrotask)
谁发起的JS引擎宿主(Node、浏览器)
具体事件Promise.then()/.catch()、async await、MutaionObserver、nextTick(Node.js 环境)script(整体代码)、setTimeout/setInterval 、UI渲染任务、事件处理器、I/O操作(如文件读取)、Ajax网络请求等异步任务
谁先执行先执行后执行
会触发新一轮事件循环吗不会
  • :是一种后进先出的数据结构,数据元素在插入(即进栈)和删除(即出栈)时均从栈顶进行操作。
    类似于堆在一起的餐盘,最先放的盘子在最底下,最后放的盘子在最上面,需要把最上面的盘子一个个拿走,才能拿到最下面的盘子。
  • 队列:是一种先进先出的数据结构,数据元素在队尾插入而从队首删除的。
    类似于我们去排队买东西,先去的同学可以先买到。

在这里插入图片描述

三、灵魂三问

1. JS为什么是单线程的?

JS引擎之所以是单线程,是由于JavaScript最初是作为浏览器脚本语言开发的,并且JavaScript需要操作DOM等浏览器的API,如果多个线程同时进行DOM更新等操作则可能会出现各种问题(如竞态条件、数据难以同步、复杂的锁逻辑等),因此将JS引擎设计成单线程的形式就可以避免这些问题。

如果JS是多线程的场景描述:
现在有2个线程,process1 process2,由于是多线程的JS,所以他们对同一个dom,同时进行操作;
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?这时可能就会出现问题了。
这样想,JS为什么被设计成单线程应该就容易理解了吧

2. 为什么需要异步? (为什么要有事件循环机制?)

如果JS中不存在异步,只能自上而下执行;如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验。所以JS中存在异步执行。

3. 单线程又是如何实现异步的呢?

异步的核心就是事件循环机制(Event Loop)。
当 JavaScript 引擎空闲下来,也就是当前的执行栈已经清空时,JavaScript 引擎才会去查询任务队列中是否有需要执行的异步任务,这就是保证异步代码不会阻塞其他任务执行的关键。

四、什么是事件循环?

事件循环是JavaScript实现异步的一种方法,也是JavaScript的执行机制。

因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。

  1. 先执行同步任务,如果遇到异步任务,js 引擎并不会一直等待其返回结果,而是会将这个任务挂起,继续执行执行栈中的其他同步任务。
  2. 当异步任务执行完毕后,再将异步任务对应的回调函数加入到一个任务队列中等待执行。
  3. 任务队列可以分为宏任务队列微任务队列,当执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有,就将微任务队首的事件压入栈中执行。队列遵循先进先出原则。
  4. 当微任务队列中的任务都执行完成后,再去执行宏任务队列中的任务。
  5. 如果宏任务队列中有微任务,继续执行微任务。如此反复循环,直至任务队列为空。这就是JavaScript的事件循环机制

总结JS代码执行顺序:同步任务 => 微任务 => 宏任务。

需要注意的点:

  1. 所有的代码都要通过函数执行栈(主线程)中调用执行。
  2. 等到执行栈中的task执行完之后再回去执行任务队列之中的task。
  3. 任务队列中存放的是回调函数
  4. 执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
  5. 当执行一个宏任务时,如果宏任务中产生了新的微任务,这些微任务不会立刻执行,而是会被放入到当前微任务队列中,在当前宏任务执行完毕后被依次执行。

JS的事件循环也可以用一张图来表示,图文结合更容易理解。
在这里插入图片描述

五、事件循环(Event Loop )执行顺序

  1. 首先执行同步代码,这属于宏任务。
  2. 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行。
  3. 执行所有微任务。
  4. 当执行完所有微任务后,如有必要会渲染页面。
  5. 然后开始下一轮 Event Loop,执行宏任务中的异步代码。

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

六、事件循环实例讲解

上面描述了事件循环机制的由来,概念。下面结合实例将概念运用起来。

实例1:

 setTimeout(function(){
     console.log('定时器setTimeout')
 },0);
 
 new Promise(function(resolve){
     console.log('同步任务1');
     for(var i = 0; i < 10000; i++){
         i == 99 && resolve();
     }
 }).then(function(){
     console.log('Promise.then')
 });
 
 this.$nextTick(() => {
    console.log('nextTick')
 })

 console.log('同步任务2');
 
// 执行结果: 同步任务1 => 同步任务2 => nextTick => Promise.then => 定时器setTimeout

分析执行过程:

  1. 首先执行 script 下的宏任务,遇到 setTimeout,0秒之后定时器运行完毕,把它放到 宏任务队列 中。
  2. 遇到 new Promise 直接执行,打印 “同步任务1”。
  3. 遇到 then 方法,属于微任务,把它放到 微任务队列 中。
  4. 遇到 nextTick,属于微任务,把它放到 微任务队列 中。在同步代码执行完成后,不管其他异步任务,先尽快执行 nextTick。
  5. 打印 “同步任务2”。
  6. 本轮宏任务执行完毕,查看本轮的微任务,发现有一个 then 方法里的函数,打印执行 “Promise.then”。
  7. 到此,第一轮的 Event Loop 全部完成。(第一轮的宏任务和宏任务下的微任务都被执行过了)。
  8. 下一轮的循环里,先执行一个宏任务,发现 宏任务队列 有一个 setTimeout 里的函数,执行打印 “定时器setTimeout”。

顺便提一下,在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms。
具体值可能不固定,但不是为 0。

实例2:

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

async function async1 () {
   console.log('async1 start')
   await async2()
   console.log('async1 end')
}
async function async2 () {
   console.log('async2')
}
async1()
    
setTimeout(() => {
    console.log('setTimeout1')
    new Promise((resolve) => {
        console.log('Promise 1')
        resolve()
    }).then(() => {
        console.log('promise.then1')
    })
}, 10)

new Promise((resolve) => {
    console.log('同步代码2, Promise 2')
    setTimeout(() => {
        console.log('setTimeout2')
    }, 10)
    resolve()
}).then(() => {
    console.log('promise.then2');
    setTimeout(() => {
        console.log('setTimeout3')
    }, 10);
    new Promise((resolve) => {
        console.log('Promise 3')
        resolve()
    }).then(() => {
        console.log('promise.then3')
    });
})

console.log('同步代码3');

/* 宏任务队列中第1个宏任务script的打印:*/
// 同步代码1
// async1 start
// async2
// 同步代码2, Promise 2
// 同步代码3
// async1 end
// promise.then2
// Promise 3
// promise.then3

/* 宏任务队列中第2个宏任务setTimeout1的打印:*/
// setTimeout1
// Promise 1
// promise.then1

/* 宏任务队列中第3个宏任务setTimeout2的打印:*/
// setTimeout2

/* 宏任务队列中第3个宏任务setTimeout3的打印:*/
// setTimeout3

需要注意的点:

  1. promise中的回调函数立刻执行,then中的 回调函数 会推入微任务队列中,等待执行栈所有任务执行完才执行。
  2. 通过 async 定义的函数在 执行栈 中执行,await 将异步程序变成同步,所以await 后面执行的程序需要等到await定义的函数执行完毕才执行,需要放入微任务队列中等待的。

七、关于setTimeout

下面这段代码我们一般会说,3秒后,执行setTimeout里的那个函数。

 setTimeout(function(){
    console.log('执行了')
 },3000)    

其实这样说并不严谨,准确的说应该是:

通过 定时触发器线程 来计时并触发定时,3秒后,setTimeout里的函数会被推入任务队列,等JS引擎线程(主线程)空闲后才会推入主线程执行。

所以只有同时满足下面两点才会3秒后执行该函数。

  • 3秒后
  • 主线程空闲时

如果主线程有很多内容需要执行,执行时间超过3秒,比如10秒,那么这个函数只能10秒后才会被推入主线程执行了。

经典面试题案例

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log('异步任务', i);
    }, 1000);
}

console.log('同步任务', i);
//执行顺序 5 => 5,5,5,5,5; 即第一个5直接输出,1秒后,输出5个5
//但是我们想要的执行顺序是5 => 0,1,2,3,4

执行结果解析:

1)首先 i = 0,满足条件,执行栈执行循环体里的代码,遇到setTimeout是异步操作,它会将回调函数推入到 任务队列 中
2)当 i = 1,2,3,4 时,均满足条件,情况和i = 0相同,因此现在任务队列中有5个相同的延时执行的函数。
3)当 i = 5 时,不满足条件,for循环结束。 console.log(‘同步任务’, i)入栈,此时i值被累加到了5。因此输出5。
4)此时1s已经过去,5个回调函数按照顺序推入任务队列。
5)主线程去执行任务队列中的函数,5个function依次入栈执行之后再出栈,此时的i已经变成了5。因此几乎同时输出5个5。
6)因此等待1s的时间其实只有输出第一个5之后需要等待1s,1s时间之后将回调函数交给任务队列。等主线程任务执行完毕后,再去执行任务队列中的5个回调函数。这期间是不需要等待1s的。
因此输出的状态就是:5 => 5,5,5,5,5,即第1个 5 直接输出,1s之后,输出 5个5;

但我们实际想要的结果是 5 => 0,1,2,3,4,所以需要再改下代码。

for (let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log('异步任务', i);
    }, 1000);
}

console.log('同步任务', i);
//执行顺序5 => 0,1,2,3,4 即第一个5直接输出,1秒后,输出0,1,2,3,4

将 var 改为 let 后,我们就可以得到期望的输出结果 0、1、2、3、4。

这是因为 let 声明的变量具有块级作用域,每次迭代都会创建一个新的 i 变量,并且在每个 setTimeout 回调函数内部,都能访问到对应的 i 变量。因此,改用 let 关键字声明 i,可以有效避免由闭包引起的变量共享问题。

八、思考问题

1. JS 中的计时器能做到精确计时吗?为什么?

并不能做到完全精确的计时。

  1. JS是单线程的,当调用 setTimeout 或 setInterval 时,通过定时器触发线程来计时并触发定时(计时完毕后,添加到任务队列中排队,等待JS引擎空闲后执行)。但是,如果此时主线程正忙于处理其他任务,那么新的计时器任务就需要等待主线程空闲后才被执行。这就有可能导致计时器任务延迟执行,甚至丢失任务。
  2. JS 本身不支持精确的时间控制,即使我们在设置计时器时传递了一个准确的时间间隔,浏览器也无法保证定时器完全精确,也就是说它们存在一定的“误差”。
  3. W3C在HTML标准规定,要求setTimeout中地域4ms的时间间隔算为4ms。

2. 为什么script(整体代码)是宏任务却优先执行?

在 JavaScript 的事件循环中,整体 script 代码也是一个宏任务。与其他宏任务不同的是,当浏览器加载 HTML 文档时,遇到 script 标签就会停止对文档的解析并立即执行脚本中的代码,因此 script 被认为是“特殊”的宏任务。
script 可能包含大量的代码,例如定义全局变量、函数、类等,或者涉及到 DOM 操作、网络请求等耗时操作。如果这些代码都被视为微任务,则会造成其他应用程序进程(如渲染进程)过长的卡顿和阻塞,影响用户体验。因此,浏览器通常会将外部 script 作为一个宏任务执行,以确保其他任务(如页面渲染)优先执行。

总结:整体 script 代码虽然是宏任务,但是包含的内容重要性高且预处理时间较长,所以是宏任务却优先执行。

可参考:
10分钟理解JS引擎的执行机制
JS事件循环机制
深入浅出Javascript事件循环机制(上)
js事件循环机制及面试题详解

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

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

相关文章

哈工大软件架构与中间件作业1

《软件架构与中间件》作业1报告 ——作业1&#xff1a;软件架构 姓名&#xff1a; 石卓凡 学号&#xff1a; 120L021011 目录 项目介绍......................................................................................................…

混淆(Proguard R8)和反混淆

本篇来介绍下Android的混淆和反混淆&#xff0c;说起混淆&#xff0c;大家都会很自然地想到Proguard&#xff0c;此外还有R8。事实上&#xff0c;AGP3.3之后&#xff0c;官方默认使用R8做代码优化、混淆和压缩。ProGuard和R8常常用于混淆最终的Android项目&#xff0c;增加项目…

【加载更多 Objective-C语言】

一、咱们上午就做了两件事儿, 1.把我们的数据,加载起来, 2.实现了下面这个”加载更多“按钮的功能, 3.只不过,我们加载数据的时候,用了一个自定义cell, 那么,基本加载数据的办法,我就不再说了, 基本,就是那些步骤, 只是把我们自定义cell部分,再给大家复习一下…

【C语言】宏实现一个整数的二进制位的奇数位和偶数位交换

要写一个宏实现将一个整数的二进制位的奇数位和偶数位交换&#xff0c;我们首先要分析如何将一个整数的二进制位的奇数位和偶数位交换 以下以整数7为例 7的二进制&#xff1a; 0000 0000 0000 0000 0000 0000 0000 0111 7 奇数位与偶数位交换后为&#xff1a; 0000 0000 0000 …

一周狂赚50万,GPT-4帮你在线“脱单”,AI女友按分钟收费,在线男友数量多达1000+

电影情节照进现实 不知道大家有没有看过一部电影《她》&#xff0c;讲述的是在不远的未来人与人工智能相爱的科幻爱情电影。主人公西奥多和人工智能系统OS1的化身萨曼莎在相处中&#xff0c;发现彼此之间都存在双向的需求与欲望&#xff0c;人机友谊最终发展成为一段不被世俗理…

Zookeeper 分布式应用程序的分布式协调服务

老规矩学习一个新技术首先从它的官网入手&#xff1a;Apache ZooKeeper 概览 一谈到集群&#xff0c; 从结构上看&#xff1a; 主从集群&#xff1a;主从集群就可以做读写分离&#xff0c;写在主、读在从无主集群&#xff08;比如redis cluster&#xff09; 从数据上看&…

10---正则表达式匹配

给你一个字符串 s 和一个字符规律 p&#xff0c;请你来实现一个支持 . 和 * 的正则表达式匹配。 . 匹配任意单个字符* 匹配零个或多个前面的那一个元素 所谓匹配&#xff0c;是要涵盖 整个 字符串 s的&#xff0c;而不是部分字符串。 示例 1&#xff1a; 输入&#xff1a;s…

JavaEE 数据链路层 以太网协议

网络原理补充-数据链路层与以太网协议 文章目录 JavaEE & 网络原理补充-数据链路层 & 以太网协议1. 以太网数据帧1.1 帧头帧尾1.2 类型1.3 载荷 2. IP数据报补充2.1 16位标识2.2 13位片偏移2.3 3位标识 3. DNS3.1 DNS原理3.2 DNS劫持或者污染 JavaEE & 网络原理补…

MongoDB 查询文档中使用文本选择器($text)

之前我们介绍过使用比较选择器、逻辑选择器、元素选择器、数组选择器查询文档&#xff0c;如果您需要进一步了解&#xff0c;可以参考&#xff1a; MongoDB 查询文档中使用比较选择器、逻辑选择器https://blog.csdn.net/m1729339749/article/details/129965699 MongoDB 查询文…

IHS安装ssl证书

1、向专业机构申请证书&#xff0c;或者使用openssl生成自签名证书&#xff0c;openssl生成证书参考以下步骤。 openssl生成证书参考https://blog.51cto.com/longlei/2120718 生成加密私钥 [rootlocalhost test]# openssl genrsa -out test.key 2048 Generating RSA private…

直线模组常见故障的解决方法

直线模组因其具有单体运动速度快、重复定位精度高、本体质量轻、占设备空间小、寿命长等特点&#xff0c;运用的范围一直在扩大&#xff0c;发展至今&#xff0c;已经被广泛应用到各种各样的设备当中。 在直线模组的使用过程中&#xff0c;或多或少都会出现一些问题&#xff0c…

DUBBO 3.x 兼容 invoke 调用

从DUBBO的2.7.22版本升级到了3.x的版本后&#xff0c;发现invoke失灵了 首先是启动报错&#xff0c;注释掉配置 dubbo.protocol.telnetinvoke后程序可运行&#xff0c;但是invoke失效。 通过对比源码 示例&#xff1a; tag-3.0.10 tag-2.7.22 发现3.0.2之后的版本都移除了i…

【网络编程】UDP简单实现翻译软件与网络聊天室

文章目录 一、引入二、翻译软件实现2.1 加载字典2.2 处理数据并传递给用户端2.3 客户端获取结果2.4 结果2.5 执行命名功能 三、网络聊天室实现3.1 管理用户3.2 发送消息3.3 多线程处理3.4 结果 四、源码 一、引入 在上一章【网络编程】demo版UDP网络服务器实现实现了客户端和服…

(1分钟了解)视觉惯性导航初始化方法综述

视觉惯性导航初始化方法综述 ​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 初始化相关的简介&#xff0c;在这里知道初始化方法可以分为联合初始化、非联合初始化和半联合初始化三种方法即可。 ​ 编辑切换为居中 添加图片注释&…

VIM学习笔记 正则表达式-(vimgrep/grep)

在UNIX问世的前一年&#xff0c;1969年&#xff0c;Ken Thompson将正则表达式整合入QED文本编辑器。在Linux文本编辑器ed中&#xff0c;如果你希望显示包含字母“re”的行时&#xff0c;需要使用命令g/re/p&#xff0c;而grep也因此得名。可以看作此操作的缩写&#xff1a;g (g…

ARM板上的蓝牙对讲功能

1&#xff09;ARMRTL8723 或RTL8821 RTL8723是USB接口的邮票芯片&#xff0c;集成了wifi和BT。前面已经完成了wifi的处理&#xff0c;这次主要说一下蓝牙语音方面。 蓝牙功能&#xff0c;我们主要是使用Bluez5协议栈.结合alsa使用&#xff08;pulseaudio也是可以的&#xff0c…

JVM学习(四)

1. JAVA IO/NIO 1.1. 阻塞 IO 模型 最传统的一种 IO 模型&#xff0c;即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后&#xff0c;内 核会去查看数据是否就绪&#xff0c;如果没有就绪就会等待数据就绪&#xff0c;而用户线程就会处于阻塞状态&#xff0c…

Class 02 - R语言Rstudio的安装

Class 02 - R语言&Rstudio的安装 下载和安装R安装前准备下载R语言安装R语言开始使用R语言 下载和安装RStudio安装前准备下载RStudio安装RStudio开始使用RStudio如何编写代码 下载和安装R 在这个部分中&#xff0c;你将完成在计算机上下载和安装R语言程序。当安装完成后&am…

嫌视频背景杂乱或单调?如何去除视频杂乱背景

录制视频时&#xff0c;有时候视频背景会十分杂乱或单调&#xff0c;这会对用户的视觉体验和注意力产生负面影响。 背景杂乱或单调的场景可能会影响您的观感。这种情况通常发生在自然光线不足或拍摄环境不理想的情况下。如果您想改变视频的背景或者是去除视频杂乱的背景&#…

硬件I2C读写MPU6050代码

1、接线图 SDA接在B11,SCL接在B10 &#xff0c;软件IIC的两个引脚可以任意更改的&#xff0c;因为都是开漏输出&#xff0c;硬件接在哪个引脚上&#xff0c;程序中就对应操作哪个引脚 但是硬件IIC&#xff0c;通信引脚是不可以任意指定的&#xff0c;查表&#xff0c;由于PB6、…