本文主要讲解关于JS事件循环(Event Loop)改了?宏任务不存在了?让我们来看看最新的是什么样的相关内容,让我们来一起学习下吧!
引言
众所周知,JavaScript是一门单线程语言,它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
但是渲染主线程承担着诸多工作如:渲染页面等。
如果所有代码都是同步的话,如果一个代码卡住了,就会导致主线程代码的阻塞,从而导致运行性能效率受到极大的影响。
所以浏览器采用了异步的方式,来避免这种情况的发生。具体是当某些任务发生时,如计时器、网络、事件监听等,主线程将任务交给其他线程去计时或监听,当主线程无事可做时或计时监听触发时,将任务加入到消息队列的末尾排队,等待主线程调度。
宏任务与微任务
在过去的JavaScript中,异步任务又分为宏任务和微任务。
JavaScript 引擎遵循事件循环的机制,在执行完当前宏任务后,会检查微任务队列,执行其中的微任务,然后再取下一个宏任务执行。这个过程不断循环,形成事件循环。
1、宏任务(Macrotasks)是一些较大粒度的任务,包括:
- 所有同步任务
- I/O操作,如文件读写、数据库数据读写等
-
setTimeout
、setInterval
-
setImmediate
(Node.js环境) requestAnimationFrame
- 事件监听回调函数等
- ...
2、微任务(Microtasks)是一些较小粒度、高优先级的任务,包括:
-
Promise
的then
、catch
、finally
-
async/await
中的代码 -
Generator
函数 MutationObserver
-
process.nextTick
(Node.js 环境) - ...
事件循环(Event Loop)
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在Chrome中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入队列末尾。
过去把消息队列简单分为宏任务和微任务,但是随着浏览器复杂度急剧提升,W3C已经不再使用宏任务的说法。
根据W3C的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列里,不同类型的任务分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同队列中取出任务执行。
大概有以下几个的队列DOM操作
、用户交互
、网络请求
、网页导航和历史记录遍历
、渲染
查看原文
- 同时浏览器必须准备好一个微队列,微队列中的任务优先于所有其他任务执行。查看原文
在目前的Chrome实现中,至少包含了以下队列:
队列类型 | 描述 | 优先级 |
---|---|---|
微队列 | 用于存放需要最快执行的任务 | 最高 |
交互队列 | 用于存放用户操作后产生的事件处理任务 | 高 |
延时队列 | 用于存放计时器到达后的回调任务 | 中 |
网络队列 | 用于处理网络活动产生的任务 | 中 |
此处的优先级仅方便于我们对于谷歌浏览器队列的实现的方便展现,并未在W3C中强约束优先级,所以每个浏览器的实现方式可能些许不同
演示
那有人就会问了,如果是按照这个说法,很多关于消息队列的面试题岂不是会混乱,这么大的事情应该大家都知道吧,在实际开发中,有什么区别呢,我们上代码试试。
- 首先,我们需要一个阻塞主线程的函数
// delay函数 传入一个时间,使用循环,阻塞主线程的运行,方便我们后续的操作
function delay(duration) {
const now = Date.now();
while (Date.now() - now < duration) { }
}
- 网络队列,在网络队列中,我们通过调用fetch然后加上delay函数,让函数进入队列。
function addNetWork() {
fetch('./1.json').then((res) => {
console.log('网络队列执行');
})
console.log('添加网络队列');
delay(2000)
}
- 延时队列,我们使用setTimeout函数加上delay,延时为0,让函数进入队列。
function addDelay() {
setTimeout(() => {
console.log('延时队列执行');
}, 0)
console.log('添加延时队列');
delay(2000)
}
- 交互队列,我们给一个按钮绑定onclick事件,然后再绑定事件后,点击按钮,让函数进入队列。
function addInteraction() {
interaction.onclick = function () {
console.log('交互队列执行');
}
console.log('添加交互队列');
delay(2000)
}
- 为了方便,我们需要一个主函数去触发上面三个方法。
begin.onclick = function () {
addDelay()
addNetWork()
addInteraction()
console.log('添加成功');
}
因为node和浏览器实现事件循环的方式不同,所以我们使用谷歌浏览器(v123.0.6312.106)测试
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<button id="begin">开始</button>
<button id="interaction">添加交互任务</button>
<body>
<script>
const begin = document.querySelector("#begin");
const interaction = document.querySelector("#interaction");
// delay函数 传入一个时间,使用循环,阻塞主线程的运行,方便我们后续的操作
function delay(duration) {
const now = Date.now();
while (Date.now() - now < duration) { }
}
function addDelay() {
setTimeout(() => {
console.log('延时队列执行');
}, 0)
console.log('添加延时队列');
delay(2000)
}
function addNetWork() {
fetch('./1.json').then((res) => {
console.log('网络队列执行');
})
console.log('添加网络队列');
delay(2000)
}
function addInteraction() {
interaction.onclick = function () {
console.log('交互队列执行');
}
console.log('添加交互队列');
delay(2000)
}
begin.onclick = function () {
addDelay()
addNetWork()
addInteraction()
console.log('添加成功');
}
</script>
</body>
</html>
分析
此时,我们使用宏任务与微任务的方式对我们的队列进行分析
预期的打印结果是
- 添加延时队列
- 添加网络队列
- 添加交互队列
- 添加成功
- 延时队列执行
- 网络队列执行
- 交互队列执行
但是实际运行结果如下(注意:在控制台打印添加交互队列时,点击了添加交互任务按钮)
我们发现与我们之前预期的宏任务微任务结果不一致,交互队列执行代码早于延时与网络队列。所以可以证明,浏览器对于不同类型的队列,有不同的处理时机。这里预测是因为浏览器希望用户点击后,可以尽快响应,而不用等待延时或者网络等队列的执行。所以也可以证明谷歌浏览器实现已经不是简单的宏任务与微任务了。
新的分析图应该为:
拓展
那我们还可以提出问题,那网络队列和延时队列之间呢?
我们交换一下启动函数的代码顺序:
begin.onclick = function () {
addNetWork()
addDelay()
addInteraction()
console.log('添加成功');
}
此时控制台变为:
- 添加网络队列
- 添加延时队列
- 添加交互队列
- 添加成功
- 交互队列执行
- 网络队列执行
- 延时队列执行
我们可以看到,网络和延时队列的执行还是按照宏任务的方式,谁先进入队列,谁先运行。他们的优先级是相同的。
那微队列有没有变化呢?
我们修改一下代码,在我们运行队列时添加一个微队列,完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<button id="begin">开始</button>
<button id="interaction">添加交互任务</button>
<body>
<script>
const begin = document.querySelector("#begin");
const interaction = document.querySelector("#interaction");
// delay函数 传入一个时间,使用循环,阻塞主线程的运行,方便我们后续的操作
function delay(duration) {
const now = Date.now();
while (Date.now() - now < duration) { }
}
// 创建一个微队列任务
function microTask() {
Promise.resolve().then(() => {
console.log('微队列执行');
})
}
function addDelay() {
setTimeout(() => {
console.log('延时队列执行');
microTask()
}, 0)
console.log('添加延时队列');
delay(2000)
}
function addNetWork() {
fetch('./1.json').then((res) => {
console.log('网络队列执行');
microTask()
})
console.log('添加网络队列');
delay(2000)
}
function addInteraction() {
interaction.onclick = function () {
console.log('交互队列执行');
microTask()
}
console.log('添加交互队列');
delay(2000)
}
begin.onclick = function () {
addNetWork()
addDelay()
addInteraction()
console.log('添加成功');
}
</script>
</body>
</html>
执行结果是:
- 添加网络队列
- 添加延时队列
- 添加交互队列
- 添加成功
- 交互队列执行
- 微队列执行
- 网络队列执行
- 微队列执行
- 延时队列执行
- 微队列执行
我们可以看到对于微队列的执行是没有变化的,当微队列有事件时,会先执行微队列中的事件。
结论
在W3C中对于事件循环(Event Loop)的处理已经不再是之前的宏任务与微任务的方式了,每个任务都有一个任务类型,同一个类型的任务必须在一个队列里,不同类型的任务分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同队列中取出任务执行。但是浏览器也必须准备好一个微队列(microtask),微队列中的任务优先于所有其他任务执行。
写在最后
以上内容是通过查阅W3C文档和参考了渡一的前端大师课,只测试了谷歌浏览器的实现,感兴趣的可以试试其他浏览器的实现。部分概念引用了掘金其他关于事件循环的文章,链接如下:
- # JavaScript事件循环机制(event loop)
- # JS的执行原理,一文了解Event Loop事件循环、微任务、宏任务
本人刚进入前端行业,对于文章内容有问题的可以交流评论。有错误也请指正。 感恩~
(大三正在找实习,有兴趣的可以联系我~)
以上就是关于JS事件循环(Event Loop)改了?宏任务不存在了?让我们来看看最新的是什么样的相关的全部内容,希望对你有帮助。欢迎持续关注程序员导航网,学习愉快哦!