从一道经典题来弄懂Eventloop(搞不懂算我输)

news2024/12/25 0:39:16

前言

时间不知不觉来到了11月底,马上也要准备一下寒假的实习了。 最近打算把面试中的一些拦路虎给解决掉!!

先拿臭名昭著的Eventloop开刀~

经典题

 async function foo() {console.log('foo')
}
async function bar() {console.log('bar start')await foo()console.log('bar end')
}
console.log('script start')
setTimeout(function () {console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {console.log('promise executor')resolve();
}).then(function () {console.log('promise then')
})
console.log('script end') 

今天刚准备手刀Eventloop的时候,差点就被这个题劝退了😭😭

但现在我终于能拿捏它了~😁😁

Eventloop(事件循环):是怎么让页面活起来的

我们都知道主线程是非常繁忙的,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务。

这个统筹调度系统就是消息队列事件循环系统

使用单线程处理安排好的任务

我们先从最简单的场景讲起,比如有如下一系列的任务:

任务 1:1+2

任务 2:20/5

任务 3:7*8

任务 4:打印出任务 1、任务 2、任务 3 的运算结果

在上面的执行代码中,我们把所有任务代码按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出。可以参考下图来直观地理解下其执行过程:

在线程运行过程中处理新任务

但并不是所有的任务都是在执行之前统一安排好的,大部分情况下,新的任务是在线程运行过程中产生的。比如在线程执行过程中,又接收到了一个新的任务要求计算“10+2”,那上面那种方式就无法处理这种情况了。

要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制

处理其他线程发送过来的任务

面我们改进了线程的执行方式,引入了事件循环机制,可以让其在执行过程中接受新的任务。不过在第二版的线程模型中,所有的任务都是来自于线程内部的,如果另外一个线程想让主线程执行一个任务,利用第二版的线程模型是无法做到的。

那么如何设计好一个线程模型,能让其能够接收其他线程发送的消息呢?

一个通用模式是使用消息队列。

有了队列之后,我们就可以继续改造线程模型了,改造方案如下图所示:

从上图可以看出,我们的改造可以分为下面三个步骤:

1.添加一个消息队列;2.IO 线程中产生的新任务添加进消息队列尾部;3.渲染主线程会循环地从消息队列头部中读取任务,执行任务。### 处理其他进程发送过来的任务

通过使用消息队列,我们实现了线程之间的消息通信。在 Chrome 中,跨进程之间的任务也是频繁发生的,那么如何处理其他进程发送过来的任务?你可以参考下图:

从图中可以看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面讲解的“处理其他线程发送的任务”一样了,这里就不再重复了。

所以事件循环系统大致是被这样搭建起来的。

什么是Eventloop?

回到定义,那么什么是Eventloop,它的机制是什么样的呢?

同步任务直接执行并且在执行完后出栈,继而执行异步任务,而异步任务又有微任务宏任务的划分,分别被推入进入微任务队列和宏任务队列

  • 宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行
  • 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止;当微任务队列清空后,一个事件循环结束;
  • 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。

宏任务与微任务

在这里想细说一下宏任务与微任务

宏任务

前面我们已经介绍过了,页面中的大部分任务都是在主线程上执行的,这些任务包括了:

1.渲染事件(如解析 DOM、计算布局、绘制);2.用户交互事件(如鼠标点击、滚动页面、放大缩小等);3.JavaScript 脚本执行事件;网络请求完成、文件读写完成事件。为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。

简单来说宏任务就是:

setTimeout、setInterval、 setImmediate、script(整体代码)、I/O(输入输出) 操作等

微任务

微任务是怎么产生的?

一个典型的场景是监控 DOM 节点的变化情况(节点的插入、修改、删除等动态变化),然后根据这些变化来处理相应的业务逻辑。一个通用的设计的是,利用 JavaScript 设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口。

不过这有个问题,因为 DOM 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降。

如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。

这也就是说,如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性。

那该如何权衡效率和实时性呢?

针对这种情况,微任务就应用而生了

下面我们来看看微任务是如何权衡效率和实时性的。通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。

而简单来说微任务就是: process.nextTickPromise.then()、MutationObserver、Async/Await 等

是先宏再微还是先微再宏

因为我发现很多小伙伴在准备这一块的内容时,看了一下文章视频,可能就只死记硬背:先微任务再宏任务

当然我相信你这样记,肯定对你解题没错的。 但是上面我们站在微任务是如何产生时说了,原理上是先执行宏任务再进行微任务的,而且结果都没有错,这是为什么?

这是因为认为先微任务再宏任务的同学已经把script(整体代码)作为一个宏任务先判断掉了,所以后面直接找微任务就行了。

但个人觉得最好还是从原理出发进行理解先执行宏任务再进行微任务比较好,而且也就多了判断script(整体代码)这一点而已了~

async/await

要想把那道题磕出来,还得掌握async/await。

async

我们先来看看 async 到底是什么?

根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。对 async 函数的理解,这里需要重点关注两个词:异步执行隐式返回 Promise

关于异步执行的原因,我们一会儿再分析。这里我们先来看看是如何隐式返回 Promise 的,你可以参考下面的代码:

 async function foo() {return 2
}
console.log(foo())// Promise {<resolved>: 2} 

执行这段代码,我们可以看到调用 async 声明的 foo 函数返回了一个 Promise 对象,状态是 resolved,返回结果如下所示:

 Promise {<resolved>: 2} 

await

我们知道了 async 函数返回的是一个 Promise 对象,那下面我们再结合文中这段代码来看看 await 到底是什么。

 async function foo() {console.log(1)let a = await 100console.log(a)console.log(2)
}
console.log(0)
foo()
console.log(3) 

我们先站在协程的视角来看看这段代码的整体执行流程图:

如果不太理解协程是什么的话,可以自己查找资料或者参考李兵老师的课程👉👉20 | async/await:使用同步的方式去写异步代码 (geekbang.org)

首先,执行console.log(0)这个语句,打印出来 0。

紧接着就是执行 foo 函数,由于 foo 函数是被 async 标记过的,所以当进入该函数的时候,执行 foo 函数中的console.log(1)语句,并打印出 1。

接下来就执行到 foo 函数中的await 100这个语句了,这里是我们分析的重点,因为在执行await 100这个语句时,JavaScript 引擎在背后为我们默默做了太多的事情,那么下面我们就把这个语句拆开,来看看 JavaScript 到底都做了哪些事情。

当执行到await 100时,会默认创建一个 Promise 对象,代码如下所示

 let promise_ = new Promise((resolve,reject){resolve(100)
}) 

然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。

主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。

接下来继续执行父协程的流程,这里我们执行console.log(3),并打印出来 3。随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数,如下所示:

 promise_.then((value)=>{ //回调函数被激活后//将主线程控制权交给foo协程,并将vaule值传给协程
}) 

该回调函数被激活以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。

foo 协程激活之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句,执行完成之后,将控制权归还给父协程。

所以处理async和await的时候只需要记到: 其中 await 前面的代码 是同步的,调用此函数时会直接执行;而 await foo(); 这句可以被转换成 Promise.resolve(foo())await 后面的代码 则会被放到 Promise 的 then() 方法里。

当然因为这里分析的比较详细,简单一点分析可以参考下面:

1.console.log(0)2.执行foo()函数,打印 console.log(1)3.await后面的语句添加到微任务中[ console.log(a) console.log(2)]4.打印console.log(3)5.当前宏任务执行完毕,执行微任务 console.log(a) console.log(2)秒杀经典题

现在我们再来梳理一下现在解题的方法

Eventloop处理流程

  • 宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行
  • 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止;当微任务队列清空后,一个事件循环结束;
  • 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。

宏任务

setTimeout、setInterval、 setImmediate、script(整体代码)、I/O(输入输出) 操作等

微任务 process.nextTickPromise.then()、MutationObserver、Async/Await 等

async/await

  • await 前面的代码 是同步的,调用此函数时会直接执行;
  • await foo() 这句可以被转换成 Promise.resolve(foo())(同步);
  • await 后面的代码 则会被放到 Promise 的 then() 方法里,加入微任务队列 上题!!
async function foo() {console.log('foo')// 3
}
async function bar() {console.log('bar start') // 2await foo()console.log('bar end')// 6
}
console.log('script start')// 1
setTimeout(function () {console.log('setTimeout')// 8
}, 0)
bar();
new Promise(function (resolve) {console.log('promise executor') // 4resolve();
}).then(function () {console.log('promise then')// 7 
})
console.log('script end') // 5 

分析流程:

1.foo函数和bar函数都是对函数的定义,并不会执行(过)
2.console.log(‘script start’) 直接打印
3.接着将 settimeout 添加到宏任务队列,此时宏任务队列为 ['settimeout']
4.执行bar函数
5.console.log(‘bar start’) 直接打印
6.执行foo函数
7.console.log(‘foo’) 直接打印
8.console.log(‘bar end’) 加入微任务队列['bar end']
9.new Promise内部是同步代码,所以直接打印 console.log(‘promise executor’)
10.new Promise().then()是微任务,所以再将 console.log(‘promise then’) 加入微任务队列['bar end','promise then']
11.直接打印console.log(‘script end’)
12.当前宏任务中执行完了,就去找微任务['bar end','promise then'],依次输出
13.这一次的事件循环结束,开始执行下一轮的宏任务['settimeout']

是不是发现这种题真的很简单,套路都是套路。

😋😋😋

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

RabbitMQ:基本消息模型

单生产单消费模型&#xff0c;即基本消息模型或简单消费模型&#xff0c;即完成基本的一对一消息转发。 RabbitMQ 单生产单消费模型主要有以下三个角色构成&#xff1a; 生产者&#xff08;producer/ publisher&#xff09;&#xff1a;一个发送消息的用户应用程序。消费者&…

JS面试题--JavaScript数据类型

数据类型 1.JavaScript有哪些数据类型&#xff0c;它们的区别&#xff1f; JavaScript共有八种数据类型&#xff0c;分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。 其中 Symbol 和 BigInt 是ES6 中新增的数据类型&#xff1a; ● Symbol 代表…

Linux系统基础——程序和进程

代码&#xff0c;程序&#xff0c;进程 特此说明: 刘超的趣谈linux操作系统是比较重要的参考资料&#xff0c;本文大部分内容和图片来源于这个专栏。 1 实验环境 运行一个demo&#xff0c;主要功能是主进程通过系统调用fork一个新的进程&#xff0c;子进程功能是加载二进制文件…

背包模型~

背包模型 概述 ​ 最长上升子序列&#xff1a;序列DP&#xff08;相邻两个被选择的有关系&#xff09; 背包问题&#xff1a;组合DP&#xff0c;在全局的考虑之下最小 f[i][j]&#xff1a;i 表示搞了多少&#xff0c;j 表示限制 集合&#xff1a;所有仅仅从前 i 个物品当…

论文推荐:CCNet用于语义分割的交叉注意力

CCNet&#xff0c; Transformer递归交叉自注意力&#xff0c;比非局部神经网络更有效。华中科技大学、地平线、ReLER 和伊利诺伊大学香槟分校联合研发 论文提出了交叉网络 (CCNet)&#xff0c;对于每个像素&#xff0c;CCNet 中的一个新的交叉注意力模块收集其交叉路径上所有像…

智慧路口:未来都市的智能节点

摘要交通路口是部署未来智慧城市的计算、通信和情报服务的最合适地点。需要收集和处理的大量数据&#xff0c;再加上隐私和安全问题&#xff0c;促使边缘计算范式的使用&#xff0c;这种范式与大都市的物理交叉路口很好地吻合。本文主要针对高带宽、低时延的应用&#xff0c;在…

2007-2022年消费者信心、满意度、预期指数月度数据(CCI、CEI、CSI、CGPI)

根据企业商品价格指数与消费者信心指数、消费者满意指数、消费者预期指数的月度数据&#xff0c;可以探究商品价格对消费者信心的影响作用和作用机制。 商品价格与消费者信心的三类细分指数均具有滞后二阶关联&#xff0c;其中&#xff0c;商品价格对对消费者预期指数的影响作…

IndexedDB的包装器JsStore - 实现登录功能及事务处理

JsStore是IndexedDB的包装器。它提供了简单的SQL像api&#xff0c;这是容易学习和使用。 IndexedDb查询可以在web worker内部执行&#xff0c;JsStore通过提供一个单独的worker文件来保持这种功能。 最近有位叫Pioneer网友一直在问我关于事务的实现方式&#xff0c;关于…

谷粒学院——Day13【微信扫描登录】

OAuth2 OAuth2的使用场景 一、OAuth2解决什么问题 1. OAuth2提出的背景 照片拥有者想要在云冲印服务上打印照片&#xff0c;云冲印服务需要访问云存储服务上的资源。 2. 图例 资源拥有者&#xff1a;照片拥有者。 客户应用&#xff1a;云冲印。 受保护的资源&#xff…

Python pandas库|任凭弱水三千,我只取一瓢饮(2)

上一篇链接&#xff1a; Python pandas库&#xff5c;任凭弱水三千&#xff0c;我只取一瓢饮&#xff08;2&#xff09;_Hann Yang的博客-CSDN博客 I~Q&#xff1a; Function10~25 Types[Function][9:25] [infer_freq, interval_range, isna, isnull, json_normalize, lres…

④【Maven】Maven的构建命令

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ Maven的构建命令一、注意二、&#x1f680;清理…

指针与数组的联系与区别【一万六千字超详解】

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;《初识C语言》 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言数组的性质1.1 数组的内存布局1…

第四章 Spring的基础用法

文章目录 Spring的起源和背景理解依赖注入Spring容器理解Spring容器中的Bean管理容器中的Bean及其依赖注入自动装配使用Java类进行配置管理使用静态工厂、实例工厂创建Bean实例抽象Bean与子Bean容器中的工厂Bean管理Bean的生命周期几种特殊的依赖注入Spring的简化配置SpEL的功…

kali中间人攻击

数据来源 一、中间人攻击原理 1. 利用的ARP协议的漏洞 2. ARP协议原理&#xff1a; 1&#xff09;发送ARP广播请求目标MAC地址 2&#xff09;目标主机发送ARP单播应答&#xff0c;响应MAC地址 3. ARP攻击原理 攻击人通过发送虚假的ARP应答实现ARP缓存投毒!而受害人没有办法进行…

文件存储案例

1.文件存储-File文件存储案例 1.1.案例要求 1.2参考代码 文件读取 百度安全验证 文件最终的保存的目录在/data/data/user/0/包/files下&#xff08;1&#xff09;布局文件 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android&q…

kubernetes中使用Service反向代理外部服务

当我们的某个服务在外部集群的时候&#xff0c;但是又想k8s集群内的应用连接它&#xff0c;这是可以创建一个service&#xff0c;用service代理外部服务&#xff0c;然后集群内就能连接该service&#xff0c;从而间接的访问外部服务。 创建一个service代理外部的服务 创建一个…

代码挑战画 魔法圣诞树

一、前言 本文会基于C# GDI技术 从零到一 实现一颗 魔法圣诞树&#xff01;源码和素材在文末全部都有&#xff01; 二、魔法圣诞树 对于用代码画圣诞树&#xff0c;网上各种编程语言像python、css、java、c/c我们都有见到过了&#xff0c;那么在绘图方面&#xff0c;还有一位…

FastDDS(6)核心库综述

Fast DDS(前身为Fast RTPS)是DDS规范的高效高性能实现,DDS规范是一种用于分布式应用软件的以数据为中心的通信中间件(DCPS)。本次回顾Fast DDS的体系结构、操作和关键特性。 架构 Fast DDS的架构如下图所示,其中可以看到具有以下不同环境的层模型。 Application layer应…

人员工装未穿戴识别检测 opencv

人员工装未穿戴识别检测基于OpenCvyolo计算机视觉深度学习技术对现场画面中人员行为着装穿戴实时监测识别&#xff0c;发现不按要求着装违规行为立即抓拍存档同步后台。OpenCV-Python使用Numpy&#xff0c;这是一个高度优化的数据库操作库&#xff0c;具有MATLAB风格的语法。所…

RabbitMQ 第一天 基础 1 MQ的基本概念 1.4 MQ 的劣势 1.5 常见的MQ 产品

RabbitMQ 【黑马程序员RabbitMQ全套教程&#xff0c;rabbitmq消息中间件到实战】 文章目录RabbitMQ第一天 基础1 MQ的基本概念1.4 MQ 的劣势1.4.1 MQ 的劣势1.4.2 小结1.5 常见的MQ 产品第一天 基础 1 MQ的基本概念 1.4 MQ 的劣势 1.4.1 MQ 的劣势 从远程调用 到 利用 MQ 作…