前序
众所周知,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中强约束优先级,所以每个浏览器的实现方式可能些许不同。Chrome中队列的优先级可以查看task_type.h文件
演示
那有人就会问了,如果是按照这个说法,很多关于消息队列的面试题岂不是会混乱,这么大的事情应该大家都知道吧,在实际开发中,有什么区别呢,我们上代码试试。
首先,我们需要一个阻塞主线程的函数
// 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和浏览器实现事件循环的方式不同,所以我们使用Chrome浏览器测试
完整代码如下:
<!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将原来的宏任务更加细粒度,将其拆分为了多个队列,每个队列根据浏览器差异设置优先级。比如在Chrome中有网络队列、延时队列、交互队列 而优先级为:微任务 > 宏任务(交互队列 > 网络队列 = 延时队列)