上一篇文章中我们了解了什么是回调,也知道了什么是同步回调和异步回调,学习了一个XMLHttpRequest的完整流程,这次我们再说一下宏任务和微任务的概念
随着浏览器的应用越来越广泛,消息队列这种粗粒度的事件管理机制已经不能满足部分领域的需求了,迫切需要更精细的管理机制 - 微任务, 它可以在实时性和效率之间做一个有效的权衡
基于微任务的技术主要又MutationObserver、Promise等,微任务和宏任务之间有什么区别呢?
宏任务
页面中的大部分任务都是在主线程上执行,主要有:
- 渲染事件(解析DOM, 计算布局, 绘制)
- 用户交互事件
- Javascript脚本执行事件
- 网络请求、文件读取
为了协调这些任务,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列,主线程采用for循环,不断取出任务并执行,这些消息队列中任务就叫宏任务
采用事件循环系统来执行消息队列中的任务,大致步骤如下:
- 先从多个消息队列中选出一个最老的任务,这个任务称为oldestTask
- 然后循环系统记录任务开始时间,并把这个oldestTask设置为当前正在执行的任务
- 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个oldestTask
- 最后统计执行完成的时长等信息
这个流程就是消息队列中宏任务的执行过程,宏任务可以满足我们大部分的需求,但是如果有对时间精度要求较高的需求,宏任务就不能胜任了。
前面我们说过,页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件等都随时有可能被添加到消息队列中,而且添加事件是由系统操作的,JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。为了直观理解,你可以看下面这段代码:
<!DOCTYPE html>
<html>
<body>
<div id='demo'>
<ol>
<li>test</li>
</ol>
</div>
</body>
<script type="text/javascript">
function timerCallback2(){
console.log(2)
}
function timerCallback1(){
console.log(1)
setTimeout(timerCallback2,0)
}
setTimeout(timerCallback1,0)
</script>
</html>
在这段代码中,我的目的是想通过 setTimeout 来设置两个回调任务,并让它们按照前后顺序来执行,中间也不要再插入其他的任务
setTimeout触发的回调函数都是宏任务,图中两个黄色的块就是setTimeout触发的两个定时器任务
中间的粉红色部分就是渲染引擎插在两个定时器之间的任务,如果此处的执行任务过久的话,那么就会影响后面任务的执行
所以说宏任务的时间粒度比较大,执行的时间间隔时不能精确控制的,对一些高实时性的需求就不太符合,比如DOM监听
微任务
理解了宏任务之后,再看一下微任务,前面我么们说了异步回调主要有两种方式:
- 第一种是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调。 setTimeout 和 XMLHttpRequest 都是这种方式
- 第二种方式的执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务的形式体现
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务结束之前
下面一起分析一下微任务:
当JavaScript执行一段代码的时候,V8会为其创建一个全局执行上下文,与此同时,V8也会在内部创建一个微任务列表,这个微任务列表就是存放微任务的,这个列表只有V8引擎可以内部使用,无法通过JavaScript来访问。
每个宏任务都关联了一个微任务队列,那么接下来,一起分析两个重要的时间点—微任务产生的时机和执行微任务队列的时机
微任务产生的方式有两种:
第一种是使用MutationObserver监控某个DOM节点,再通过JavaScript来修改这个节点
第二种是使用Promise,当调用Promise.resolve()或者Promise.reject()的时候,会产生微任务
这两种都会被JavaScript引擎保存在微任务队列种
再一起看一下微任务是如何被执行的
通常情况下,在当前宏任务中的JavaScript快执行完成时,也就是JavaScript引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务,WHATWG把这执行微任务的时间点称为检查点。
如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务列表中,V8引擎一直循环执行微任务队列中的任务,直到队列为空才算结束,也就说执行微任务过程中产生的新的微任务并不会推迟
参考下面示意图:
该示意图是在执行一个 ParseHTML 的宏任务,在执行过程中,遇到了 JavaScript 脚本,那么就暂停解析流程,进入到 JavaScript 的执行环境。从图中可以看到,全局上下文中包含了微任务列表。
在 JavaScript 脚本的后续执行过程中,分别通过 Promise 和 removeChild 创建了两个微任务,并被添加到微任务列表中。接着 JavaScript 执行结束,准备退出全局执行上下文,这时候就到了检查点了,JavaScript 引擎会检查微任务列表,发现微任务列表中有微任务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下文。
以上就是微任务的工作流程,从上面分析我们可以得出如下几个结论:
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列
- 微任务的执行时长会影响到当前宏任务的时长
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
function test1() {
console.log('宏任务');
setTimeout(function() {
console.log('延迟宏任务')
});
new Promise(function(resolve, reject) {
console.log('定义微任务')
resolve('执行微任务')
}).then(res=>console.log(res));
}
test1();
上面代码的执行结果是: ‘宏任务’ => ‘定义微任务’ => ‘执行微任务’ => ‘延迟宏任务’