宏任务
渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。
主线程上执行的任务,比如:
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件。
WHATWG 规范
参考资料:https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
WHATWG?
(Web Hypertext Application Technology Working Group) 网页超文本应用技术工作小组是一个以推动网络HTML 5 标准为目的而成立的组织。在2004年,由Opera、Mozilla基金会和苹果这些浏览器厂商组成。
WHATWG 规范中是怎么定义事件循环机制的?
消息队列中宏任务的执行过程:
先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
最后统计执行完成的时长等信息。
例子
为什么宏任务难以满足对时间精度要求较高的任务?
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 timerCallback(){ console.log(1) setTimeout(timerCallback2,0) } setTimeout(timerCallback,0) </script> </html>
可以 f12 打开 Performance 工具,来记录下这段任务的执行过程
两个黄色块就是 setTimeout
触发的两个定时器任务,而其中有很多一段一段的(浅红色区域)任务,这些是被渲染引擎插在两个定时器任务中间的任务。
也可以自己去写个 demo 测试一下,我测试了一下如下:
Update Layer Tree
Composite Layers
所以宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。
微任务
微任务:就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。每个宏任务都关联了一个微任务队列。
微任务是怎么产生的
第一种方式:
使用 MutationObserver (下面会讲到这个) 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
第二种方式:
使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。
通过 DOM 节点变化产生的微任务或者使用 Promise 产生的微任务都会被 JavaScript 引擎按照顺序保存到微任务队列中。
微任务队列什么时候被执行
检查点:
WHATWG 把执行微任务的时间点称为检查点。通常情况下,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
下面用图直观的看一下微任务的工作流程:
添加微任务
执行流程
上面示意图:
1、执行一个 ParseHTML 的宏任务
2、在执行过程中,遇到了 JavaScript 脚本,那么就暂停解析流程
3、进入到 JavaScript 的执行环境,并且全局上下文中包含了微任务列表
4、通过 Promise 和 removeChild 创建了两个微任务,并被添加到微任务列表中
5、JavaScript 执行结束,准备退出全局执行上下文,这时候就到了检查点
6、JavaScript 引擎会检查微任务列表,发现微任务列表中有微任务
7、等微任务队列清空之后,就退出全局执行上下文
结论:
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
- 微任务的执行时长会影响到当前宏任务的时长。
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
监听 DOM 变化方法演变
轮询检测
比如使用 setTimeout 或者 setInterval 来定时检测 DOM 是否有改变。
缺点:
如果时间间隔设置过长,DOM 变化响应不够及时;反过来如果时间间隔设置过短,又会浪费很多无用的工作量去检查 DOM,会让页面变得低效。
Mutation Event
直到 2000 年的时候引入了 Mutation Event,Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调。
优点:
解决了实时性的问题。
缺点:(逐步从 Web 标准事件中删除)
浏览器兼容性问题
IE9 不支持 Mutation Events
Webkit 内核不支持 DOMAttrModified 特性
DOMElementNameChanged 和 DOMAttributeNameChanged 在 Firefox 上不被支持
性能问题
Mutation Events 是同步执行的,它的每次调用,都需要从事件队列中取出事件,执行,然后事件队列中移除,期间需要移动队列元素。如果事件触发的较为频繁的话,每一次都需要执行上面的这些步骤,那么浏览器会被拖慢,导致页面性能问题。
Mutation Events 本身是事件,所以捕获是采用的是事件冒泡的形式,如果冒泡捕获期间又触发了其他的MutationEvents 的话,很有可能就会导致阻塞 Javascript 线程,甚至导致浏览器崩溃。
MutationObserver
MutationObserver
接口提供了监视对DOM树所做更改的能力。
MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。
在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务了。
MutationObserver 采用了“异步 + 微任务”的策略。
通过异步操作解决了同步操作的性能问题;
通过微任务解决了实时性的问题。
兼容性:
示例:
// 选择需要观察变动的节点 const targetNode = document.getElementById('some-id'); // 观察器的配置(需要观察什么变动) const config = { attributes: true, childList: true, subtree: true }; // 当观察到变动时执行的回调函数 const callback = function(mutationsList, observer) { // Use traditional 'for loops' for IE 11 for(let mutation of mutationsList) { if (mutation.type === 'childList') { console.log('A child node has been added or removed.'); } else if (mutation.type === 'attributes') { console.log('The ' + mutation.attributeName + ' attribute was modified.'); } } }; // 创建一个观察器实例并传入回调函数 const observer = new MutationObserver(callback); // 以上述配置开始观察目标节点 observer.observe(targetNode, config); // 之后,可停止观察 observer.disconnect();
注意点
WHATWG标准定义的,在WHATWG规范,定义了在主线程的循环系统中,可以有多个消息队列,比如鼠标事件的队列,IO完成消息队列,渲染任务队列,并且可以给这些消息队列排优先级。
但是在浏览器实现的过程中,目前只有一个消息队列,和一个延迟执行队列。