[javascript核心-07] 带你彻底弄懂事件循环机制、宏任务、微任务

news/2024/11/7 16:38:19/

1. 宏任务微任务与事件循环

1.1. 进程与线程

进程:计算机上所运行的程序,是操作系统管理程序的方式

线程:操作系统能够调度的最小单元

关系:进程需要至少启动一个线程执行代码,这个线程成为主线程,所以进程是线程的容器

javascript执行线程:我们说进程是线程的容器,浏览器或者 Node 环境就是js的容器进程,但js是单线程的

js是单线程的:这意味着js在一个时间片内只能执行一段程序,如果其中一段程序运行非常耗时,则会阻塞线程,因此类似网络请求等操作不交由js线程处理,而是用回调函数交由其他线程处理,何时执行回调函数?这里就要用到事件队列与事件循环机制;多线程有资源抢占的问题

浏览器进程:是多进程的,浏览器本身有多个进程;新开一个tab页面就会开启一个新的进程,如果一个网页崩溃卡死就只影响这单个进程,不会导致所有页面无法响应而需要整个浏览器强制退出

1.2. 浏览器线程

每个浏览器进程都有多个线程,而处理js的只是其中一个线程。而显示一个页面除了解析 JS 还有许多其他工作,因此需要多个线程完成不同的任务:

GUI 渲染进程:渲染和解析页面

JS 引擎线程:渲染和解析 js(只分配一个线程执行js)

定时器监听线程:监听定时器

事件监听线程:监听和处理事件绑定操作

网络请求线程:获取服务器数据和资源(同源下最多同时分配6个TCP线程)

webwork线程

1.3. 同步与异步

由于 JS 是单线程的,因此 JS 的代码大部分都是同步代码。前面的代码不执行结束,不会往下执行。因此死循环和无限递归在 JS 中是致命的。

但是 JS 必然是存在异步执行的代码,此时就必须交由其他线程去执行,而多个异步操作应该按什么顺序执行,以及如何获取执行结果,就需要专门的机制来管理,在 JS中这个机制就是事件循环机制。

JS 中的异步操作是借用浏览器的多线程机制,基于 EventLoop 事件循环机制实现单线程异步操作。

1.4. 浏览器事件循环机制

概述:js主线程将耗时操作交由其他线程处理,其他线程会将这些操作放入事件循环队列,当耗时任务得到结果后,会交由js主线程执行

浏览器加载页面时会为当前页面开启两个队列:

WebAPI 任务监听队列:监听异步队列是否可以执行

EventQueue 事件队列:所有可执行的异步任务需要在该队列中入队等待

代码中遇到异步代码,先放入WebAPI 任务监听队列,浏览器使用新的线程进行监听,然后继续向下执行同步代码;当监听到异步操作可以执行后,根据微任务和宏任务将其放入EventQueue 的不同事件队列进行入队等待。

特别的,对于定时器来说,当设定时间到了之后也不会立即执行,是会先放入事件队列,排队执行。

当同步代码都执行完毕之后,主线程空闲,开始从EventQueue按照顺序取出异步任务放入执行栈中,由主线程执行,执行完一个异步任务再取出下一个执行。

执行顺序:相对宏任务队列,优先执行微任务队列中的任务;对于相同类型的异步任务,按照进队顺序逐一出队执行。即在执行任何宏任务之前,要先保证清空微任务队列

宏任务队列:定时器、网络请求、DOM 回调(事件绑定)、UI Rendering、消息队列(MessageChannel)

微任务队列:普通回调函数、Promise.then()、async/await、MutationObserver API(监听当前 dom 元素的属性值改变)、requestAnimationFrame、IntersectionObserver(监听当前dom元素和可视窗口交叉信息发生改变)

1.5. 浏览器事件循环机制代码分析

setTimeout(()=>{console.log(1)
},20)
console.log(2)
setTimeout(()=>{console.log(3)
},10)
console.log(4)
for(let i=0;i<90000000;i++){}
console.log(5)
setTimeout(()=>{console.log(6)
},8)
console.log(7)
setTimeout(()=>{console.log(8)
},15)
console.log(9)
  1. 将定时器1放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听
setTimeout(()=>{console.log(1)
},20)
  1. 直接执行同步代码console.log(2)输出2
  2. 同样将定时器2放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听
setTimeout(()=>{console.log(3)
},10)
  1. 直接执行同步代码console.log(4)输出4
  2. 执行同步循环任务,该循环耗时较长,大概需要 70~80ms(根据硬件差异),此时定时器1 先计时完毕,放入EventQueue中的宏任务队列中。然后定时器2 先计时完毕,放入EventQueue中的宏任务队列中。需要等待该同步循环执行完毕再往下执行。
for(let i=0;i<90000000;i++){}
  1. 直接执行同步代码console.log(5)输出5
  2. 将该定时器3放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听,需要等待 8 毫秒
setTimeout(()=>{console.log(6)
},8)
  1. 直接执行同步代码console.log(7)输出7
  2. 将该定时器4放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听,需要等待 15 毫秒
setTimeout(()=>{console.log(8)
},15)

同步代码执行完毕,开始执行异步代码:

  1. 查看EventQueue中的微任务队列,当前没有需要执行的微任务
  2. 查看EventQueue中的宏任务队列,取出定时器1 并执行
  3. 查看EventQueue中的微任务队列,当前没有需要执行的微任务
  4. 查看EventQueue中的宏任务队列,取出定时器2 并执行
  5. 主线程再次空闲,查看EventQueue中的微任务队列,当前没有需要执行的微任务
  6. 查看EventQueue中的宏任务队列,当前没有需要执行的宏任务,继续轮询等待
  7. 等待8ms后(定时器 3 的计时时间),定时器 3 被放入EventQueue中的宏任务队列
  8. 主线程轮询微任务队列、宏任务队列,取出定时器3 并执行
  9. 再次等待2ms后(定时器 4 计时10ms),定时器 3 被放入EventQueue中的宏任务队列
  10. 主线程轮询微任务队列、宏任务队列,取出定时器4 并执行

1.6. 异步函数 async代码执行分析

  1. async函数中的代码是同步执行的
  2. 遇到await会立即该语句下面的代码,作为微任务放入WebAPI队列中进行监听,等待前面await语句的返回结果。只有await返回的Promise状态是成功(resolve)的,才会放入事件队列EventQueue
  3. 而当前上下文中,await语句的返回结果是一个 Promise 实例,即使返回的不是Promise,也会包装为 Promise返回。
  4. 同步代码执行完毕后,再从事件队列中取出微任务执行。
const fn = async()=>{console.log(1)return 10
}
(async function(){let result = await fn()console.log(2,result)
})()
console.log(3)
  1. await fn()函数直接执行,console.log(1)直接打印出 1。由于前面是await,因此fn函数执行的返回值会被包装为Promise返回,即new Promise.resolve(1)
  2. console.log(2,result)作为微任务放入WebAPI队列中进行监听,上面的await语句返回结果为成功状态,因此可以执行,放入事件队列EventQueue中执行
  3. console.log(3)输出3
  4. 从事件队列EventQueue中取出微任务进行执行,输出2,10

1.7. 异步函数与 Promise 的面试题

1.async函数中的代码是同步执行的,包括await语句也是同步执行

2.await后面的代码会被放入微任务队列

//02.js
async function async1 () {console.log('async1 start')await async2();console.log('async1 end')
}async function async2 () {console.log('async2')
}console.log('script start')setTimeout(function () {console.log('setTimeout')
}, 0)async1();new Promise (function (resolve) {console.log('promise1')resolve();
}).then (function () {console.log('promise2')
})console.log('script end')// script start
// async1 start
// async2
// promsie1
// script end
// async1 end
// promise2
// setTimeout
  1. console.log('script start')同步代码直接执行
  2. 定时器1 直接放入webAPI队列中进行定时监听
setTimeout(function () {console.log('setTimeout')
}, 0)
  1. async1();执行,console.log('async1 start')同步代码直接执行输出;await async2(),后面的代码console.log('async1 end')直接放入webAPI队列中进行监听,为了方便先命令为微任务 1,它要等待前面awiat的执行结果。
  2. 前面await async2()执行。async2()执行输出async2,无返回值,默认返回undefined,将其包装为promise实例,且是成功的状态,因此后面放入webAPI队列中微任务 1被放入微任务队列中等待 JS 轮询执行。
  3. 对于下面的代码,console.log('promise1')先同步执行。将then中的代码作为微任务放入webAPI队列中监听,将其命名为微任务 2。但是前面promise返回的结果是成功状态,因此将微任务 2放入微任务队列中等待 JS 轮询执行。
new Promise (function (resolve) {console.log('promise1')resolve();
}).then (function () {console.log('promise2')
})
  1. 同步执行代码console.log('script end')

此时同步代码已执行结果,目前的输出为

/*
script start
async1 start
async2
promise1
script end
*/

开始执行异步代码:

  1. 先按顺序执行微任务队列中的代码,取出微任务 1执行,console.log('async1 end'),输出结果为async1 end
  2. 取出微任务 2执行,console.log('promise2'),输出结果为promise2
  3. 微任务队列已清空,开始执行宏任务队列。
  4. 将定时完毕的定时器从webAPI队列中放入宏任务队列中,并取出执行,打印出setTimeout

此时异步代码执行完毕,异步代码的执行顺序为:

/*
async1 end
promise2
setTimeout
*/

总体执行顺序为:

// script start
// async1 start
// async2
// promsie1
// script end
// async1 end
// promise2
// setTimeout

1.8. 事件绑定与异步执行代码分析

注意:下面的代码中必须给html,body设置高度,不然点击无效。

<html lang="en">
<head><style>html,body{height:100%;}</style>
</head>
<body><script>let body = document.body;body.addEventListener('click', function(){Promise.resolve().then(()=>{console.log(1)})console.log(2)})body.addEventListener('click', function(){Promise.resolve().then(()=>{console.log(3)})console.log(4)})</script>
</body>
</html>
  1. 事件绑定放入宏任务队列,命名为宏任务1。其点击事件放入webAPI监听事件触发,这里的事件触发为click点击事件
body.addEventListener('click', function(){Promise.resolve().then(()=>{console.log(1)})console.log(2)
})
  1. 事件绑定放入宏任务队列,命名为宏任务2。其点击事件放入webAPI监听事件触发,这里的事件触发为click点击事件
body.addEventListener('click', function(){Promise.resolve().then(()=>{console.log(3)})console.log(4)
})
  1. 点击body,宏任务1和宏任务2都可以开始执行。按照绑定先后顺序放入宏任务队列中
  2. 宏任务1先执行。将then(()=>{console.log(1)})放入webAPI监听直到前面的返回状态成功。前面返回为成功状态,因此放入微任务队列,命名为微任务1。下面的同步代码直接输入2
    Promise.resolve().then(()=>{console.log(1)})console.log(2)
  1. JS 轮询微任务队列,发现微任务队列中有微任务1。先执行微任务1,输出 1
  2. JS 轮询宏任务队列。将then(()=>{console.log(3)})放入webAPI监听直到前面的返回状态成功。前面返回为成功状态,因此放入微任务队列,命名为微任务2。下面的同步代码直接输入4
  3. JS 轮询微任务队列,发现微任务队列中有微任务2。先执行微任务2,输出 3

1.9. Node 事件循环

Node 是多线程的,开启一个js线程处理js代码,其他线程处理耗时操作,将处理完的耗时操作将回调函数加入到事件队列,js通过轮询事件队列得到结果

Node 中的事件循环是通过libuv实现的,js本身无法执行的操作通过libuv进行;事件循环是js与系统调用之间的桥梁,文件 IO,数据库读取,网络 IO,定时器,子进程等操作都会在完成对应的操作后都会将对应的结果和回调函数放到事件循环(任务队列)中;事件循环会通过轮询机制不断的从任务队列中取出对应的事件(回调函数)执行

事件循环的阶段划分:

一次完成的 Tick 包括

1.定时器(Timer):执行定时器等函数

2.待定回调(Pending Callback):对某些系统操作执行回调

3.idle, prepare:系统内部使用

4.轮询(Poll):检查新的 I/O 操作;执行与 I/O 相关的回调

5.检测(check):setImmediate()执行

6.关闭的回调函数

1.10. Node 中的宏任务与微任务

宏任务:
time queue:定时器

poll queue:IOcheck queue:setImmediate()close queue:close事件

微任务:
next tick queue: process.nextTick()
other queue:Promise.then()、普通回调

执行顺序:执行任何宏任务之前,要先保证清空微任务队列
nexttick queue

other queuetime queue:定时器poll queue:IOcheck queue:setImmediate()close queue:close事件

1.11. 浏览器宏任务微任务执行顺序面试题

//01.js
setTimeout(function () {console.log("setTimeout1");new Promise(function (resolve) {resolve();}).then(function () {new Promise(function (resolve) {resolve();}).then(function () {console.log("then4");});console.log("then2");});
});new Promise(function (resolve) {console.log("promise1");resolve();
}).then(function () {console.log("then1");
});setTimeout(function () {console.log("setTimeout2");
});console.log(2);queueMicrotask(() => {console.log("queueMicrotask1")
});new Promise(function (resolve) {resolve();
}).then(function () {console.log("then3");
});// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2

then()返回不同类型值的情况

1.直接返回普通值

//03.js
Promise.resolve().then(() => {console.log(0);// 相等于直接返回resolve(4)return 4 }).then((res) => {console.log(res)})Promise.resolve().then(() => {console.log(1);}).then(() => {console.log(2);}).then(() => {console.log(3);}).then(() => {console.log(5);}).then(() =>{console.log(6);})/*
0
1
4
2
3
5
6
*/

2.返回一个实现了then()方法的对象

//04.js
Promise.resolve().then(() => {console.log(0);// 返回的是一个thenable的,多推一次微任务return {then : function(resolve){resolve(4)}}}).then((res) => {console.log(res)})Promise.resolve().then(() => {console.log(1);}).then(() => {console.log(2);}).then(() => {console.log(3);}).then(() => {console.log(5);}).then(() =>{console.log(6);})/*
0
1
2
4
3
5
6
*/

3.thenable对象实现的then方法是一个宏任务//05.js
Promise.resolve().then(() => {console.log(0);// thenable对象实现的then方法是一个宏任务return {then : function(resolve){setTimeout(function(){resolve(4)})}}}).then((res) => {console.log(res)})Promise.resolve().then(() => {console.log(1);}).then(() => {console.log(2);}).then(() => {console.log(3);}).then(() => {console.log(5);}).then(() =>{console.log(6);})/*
0
1
2
3
5
6
4
*/  

4.返回一个 Promise 对象

//06.js
Promise.resolve().then(() => {console.log(0);// 返回 Promise// 先将 Promise.resolve(4)向微任务向后推一次// 再将 Promise.resolve(4).then()向微任务向后再推一次return Promise.resolve(4)}).then((res) => {console.log(res)})Promise.resolve().then(() => {console.log(1);}).then(() => {console.log(2);}).then(() => {console.log(3);}).then(() => {console.log(5);}).then(() =>{console.log(6);})
/*
0
1
2
3
4
5
6
*/

1.12. Node 宏任务微任务执行顺序面试题

Node 微任务有两个队列, nexttick queue 与 other queue

有定时器的宏任务会在计时结束后加入到宏任务队列,但是这取决于计时时间

下面的第二个计时器是 300ms,会在最后执行,但如果将时间改为 1ms,它会比 setImmediate 更早执行,在倒数第二个打印

// 07.js
async function async1() {console.log('async1 start')await async2()console.log('async1 end')}async function async2() {console.log('async2')}console.log('script start')setTimeout(function () {console.log('setTimeout0')}, 0)setTimeout(function () {console.log('setTimeout2')}, 300)setImmediate(() => console.log('setImmediate'));process.nextTick(() => console.log('nextTick1'));async1();process.nextTick(() => console.log('nextTick2'));new Promise(function (resolve) {console.log('promise1')resolve();console.log('promise2')}).then(function () {console.log('promise3')})console.log('script end')/*
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2
*/

http://www.ppmy.cn/news/240571.html

相关文章

TCP连接管理与UDP协议

一、TCP的连接管理 1.TCP包头 2.连接的建立——“三次握手” TCP 建立连接的过程叫做握手。 采用三报文握手&#xff1a;在客户和服务器之间交换三个 TCP 报文段&#xff0c;以防止已失效的连接请求报文段突然又传送到了&#xff0c;因而产生 TCP 连接建立错误。 3.连接的释放…

关于nginx使用中的bug

一&#xff1a; 报错&#xff1a;nginx: [emerg] WSASocketW() failed (10022: An invalid argumentwas supplied) 像使用WinSCP一样进行项目部署&#xff0c;把自己的电脑当做服务器放前端压缩包dist&#xff0c;让内网-局域网内所有人可访问前端页面 首先把nginx的文件夹放…

从Java BIO到NIO再到多路复用,看这篇就够了

从一次优化说起 近期优化了一个老的网关系统&#xff0c;在dubbo调用接口rt1000ms时吞吐量提升了25倍&#xff0c;而线程数却由64改到8。其他的优化手段不做展开&#xff0c;比较有意思的是为什么线程数减少&#xff0c;吞吐量却可以大幅提升&#xff1f;这就得从IO模型说起&a…

雷蛇雷云 Razer Synapse 服务优化,禁用 Razer Game Manager 服务

Razer Synapse Service 服务依赖 Razer Game Manager 服务运行 但 Razer Game Manager 服务存在一些问题&#xff0c;比如一段时间后 CPU/内存占用上升等等&#xff0c;该服务禁用后不影响灯光及设备设置 首先退出雷云&#xff0c;结束 Razer 开头相关进程 然后以管理员身份运…

PotPlayer降噪处理和人声增强

很多本地录屏视频&#xff0c;比如老师网课的录屏&#xff0c;会把电脑自己的声音也录下来&#xff0c;听着很烦躁&#xff0c;下面是我自己用potplayer播放视频时的一些处理。 F5打开配置→声音 →关闭规格化、晶化 →关闭混响&#xff0c;打开降噪&#xff0c;门限自选 …

雷云驱动2从云服务器,Razer Synapse 2.0(雷蛇云驱动)

Razer Synapse 2.0是由雷蛇推出的一款云端网络综合性配置器。Razer Synapse 2.0的简称为雷蛇云驱动&#xff0c;使用它需要同时使用Razer的外设设备&#xff0c;它可以把用户的游戏习惯包括游戏配置、宏和其他键鼠设置同步到云端中&#xff0c;让用户可以在任何有网络的地方都可…

雷蛇手机2 android8.1,雷蛇手机2代安卓9.0/8.1安装面具ROOT方案

免费预览&#xff1a; 注意1&#xff1a;请提前备份资料&#xff0c;解锁BL会清空所有数据&#xff01; 注意2&#xff1a;请提前移除谷歌账户(设置—账户—你的谷歌账户—移除) 全套资料在教程末尾 1. 解锁BL 在手机开机状态下&#xff0c;启用开发者选项并勾选「允许USB调试」…

VS2019 C++ 多行注释与取消注释

在 Visual Studio 2019 中&#xff0c;可以使用以下键盘快捷键进行成块注释&#xff1a; 按下 Shift Alt A 进入列选择模式。 选中需要注释的代码块。 按下 Ctrl K&#xff0c;Ctrl C 进行批量注释。 如果需要取消注释&#xff0c;可以使用 Ctrl K&#xff0c;Ctrl U 进…