前言
我们先来看个实际中的场景:
在浏览器上打印出1~100万。
对于这个需求,我们第一个反应的代码是这样的:
for (let i = 0; i < 100 * 10000; i++) {
const div = document.createElement("div");
div.innerText = i;
document.body.append(div);
}
看着好像没问题,当然这里有的同学会说,你这里操作了100万次dom,性能损耗很严重。这是一个问题,但是这个不是本文讨论的重点。
我们实际运行一下这段代码,发现一开始的时间(很短)会无法正常做交互,比如鼠标悬浮,选中文字等操作。那么为什么会造成这个现象呢?不急,我们一步步分析。
浏览器的渲染机制
以Chrome为例,一个标签页独占一个渲染进程,而JavaScript解释线程是属于这个渲染进程内的。
我们先聊下前置知识,JavaScript执行和屏幕渲染是互斥的,原因是屏幕渲染需要根据DOM结构,而JavaScript是有能力修改DOM的,所以会在JavaScript执行完毕后,执行渲染动作。一般来说,屏幕的渲染是60HZ,差不多16ms需要切换一张图片,人眼才不会意识到卡顿。
那么我们再回来看刚刚的代码
for (let i = 0; i < 100 * 10000; i++) {
}
结论很明显了,这句代码的执行明显超过了16ms!!!
事件循环机制
事件循环机制(Event Loop)就是为了解决这个问题而提出的。
简单来说,我们把大任务切成了多个小任务,让这些任务的执行和渲染流程交错进行。
计算100万个(1s) => 屏幕渲染
计算10万个(0.1s) => 屏幕渲染 => 计算10万个(0.1s) => 屏幕渲染 => 计算10万个(0.1s) => ...
这里就不贴出具体的代码实现了,感兴趣的同学可以自己动手写一下。
再探事件循环机制
事件循环机制(Event Loop)出现后,JavaScript的执行任务分为了两类:同步任务和异步任务。而异步任务,又分成了两类:
- 宏任务(Macrotask):
整体代码、定时器、I/O - 微任务(Microtask):
Promise、MutationObserver、observer
这里我们需要注意一点,Promise本身是同步代码,但是它的回调then catch是异步
new Promise((res, rej) => {
res('ok') // 同步任务
}).then((result) => {
console.log(result) // 异步任务中的微任务
})
其实我们会好奇,为什么是分成这两类?为什么不是三类或者一类?其实这是一种权衡的策略。宏任务的定义为耗时长的任务,微任务为耗时短的任务。在实际执行中,需要执行的任务分成多,所以就会有优先级的问题,其实宏任务和微任务的制定就是一种折中,为了权衡执行时间和运行效率。
优先级问题
我们刚刚提到了优先级的问题,那我们就来展开聊聊。一般情况下,微任务的执行优于宏任务。为什么是一般情况下呢?我们来看下这段代码:
for(let i = 0; i < 10; i++) {
setTimeout(() => {
console.log('宏任务开始')
for (let j = 0; j < 10; j++) {
microtask();
}
})
}
function microtask() {
return new Promise(async (res) => {
console.log('微任务开始')
await microtask();
res();
})
}
注册10个宏任务 => 运行第1个宏任务 => 注册10个微任务 => 微任务执行过程中又注册了微任务 => 微任务执行过程中又注册了微任务 => ...
这是一个微任务的死循环,如果按微任务的执行优于宏任务的论据,宏任务是不可能触发第二个的,因为微任务队列一直在被push。
但是实际情况下,我们是可以看到第二个宏任务开始的。这是为什么呢?其实是Chrome V8做的一个小策略,在微任务过多的时候,会执行下一个宏任务。
小结
我们通过了开头的一个小例子,引入了事件循环机制(Event Loop),而执行任务分为了两类:同步任务和异步任务。而异步任务,又分成了两类:宏任务和微任务。一般情况下,微任务的执行优于宏任务。当在极端场景下,比如微任务实在是太多了,Chrome V8会先执行宏任务。
最后打波小广告,美团校招社招内推,不限部门,不限岗位,不限投递数量,海量hc,快来快来~