Open jin5354 opened 7 years ago
写的很好啊,除了process.nextTick不是microtask。
@zhanba process.nextTick不是microtask吗?看各种任务队列的文章,都将它放入了microtask中。。
@jin5354 博主,问一个问题,在最后的时序图里,为什么第一个DOM内部的事件有onClick和click两个?分别代表的是什么?
@zhanba 多数文章都将 process.nextTick 放入 microtask 中。但我知道 node 中的 eventloop 与浏览器中的 eventloop 大不相同,我对 node 环境了解尚不深刻,无力堪明,若您有相关文章欢迎推荐给我
@Stone-Wang
onClick 是注册在 DOM 上的事件处理函数
click 指的是第二个测试中,手动去触发事件的 $inner.click()
函数,运行时该函数会进入执行栈中
@jin5354 所以通过点击inner和手动触发$inner.click(),区别是后者会往执行栈中推入click方法?
@jin5354 @Stone-Wang 可以看https://www.zhihu.com/question/55557148, https://github.com/creeperyang/blog/issues/21,https://github.com/creeperyang/blog/issues/26,根据node源码解读process.nextTick。 只能说网上的很多文章可能都有相互参考吧。个人觉得介绍microtask最好还是别加上process.nextTick。毕竟楼主的文章是介绍的web,没必要扯上node啊,node可以单独讲嘛
@Stone-Wang 是的,后者会往执行栈中推入 $inner.click() 方法,且在两次 onClick 执行完毕后才出栈。正是 click 在栈中存在的原因,才导致两次测试 promise 的输出时机不同,从而推论出 microtask 执行的时机。
@zhanba 确实是,node和浏览器对于任务队列的优先机制存在分歧,应该分开讲,我开始就因为笼统地看,经常把自己看懵:怎么一会可以一会又不可以,优先级还不一样。哈哈
@zhanba @Stone-Wang 恩,node 和浏览器机制不一样,不能简单的把 process.nextTick 算为 microtask,感谢提供的资料哈
@jin5354 哈哈,可以在国内推一下,很棒的文章
一个字一个字的看完了,豁然开朗,受益匪浅,终于明白了requestAnimationFrame细节,感谢博主!
@Stone-Wang 兄弟,头像和我一样啊。。。感动
title: 深入探究 eventloop 与浏览器渲染的时序问题 categories:
requestAnimationFrame toc: true date: 2017-7-18 10:38:11
在上篇文章《现代前端科技解析 —— 数据响应式系统 (Data Reactivity System)》中我们用到了
nextTick
函数,该函数使用MutationObserver
实现了『异步』更新。我们工作中也常用setTimeout(fn, 0)
来实现任务的『延迟』执行。本文以次为引,结合浏览器渲染,全面解析一轮eventloop(事件循环)
各步骤的执行时序问题。1. 引子:那些『延迟』执行的函数们
先列举几个会『延迟』执行的函数:
如果这些函数一起使用,你能猜出结果吗?
结果是:
在线示例
2. 挖掘 eventloop 规范
为了协调事件、用户交互、脚本、UI 渲染、网络请求,用户代理必须使用 eventloop。我们在 HTML5 规范中找到 eventloop 相关章节,查看每轮 eventloop 是如何执行的。
将关键点翻译成中文:
1-5. 从 task 队列(一个或多个)中选出最老的一个 task,执行它。
6. 执行 microtask 检查点。简单说,会执行 microtask 队列中的所有 microtask,直到队列为空。如果 microtask 中又添加了新的 microtask,直接放进本队列末尾。
7. 执行 UI render 操作: 7.1-7.4. 判断 document 在此时间点渲染是否会『获益』。浏览器只需保证 60Hz 的刷新率即可(在机器负荷重时还会降低刷新率),若 eventloop 频率过高,即使渲染了浏览器也无法及时展示。所以并不是每轮 eventloop 都会执行 UI Render。 7.5-7.9. 执行各种渲染所需工作,如 触发 resize、scroll 事件、建立媒体查询、运行 CSS 动画等等 7.10. 执行 animation frame callbacks 7.11. 执行 IntersectionObserver callback 7.12. 渲染 UI
用一张图来概括整体流程:
2.1 task(macrotask)
何为 task?task 又称 macrotask。我们查看 HTML 规范中 task 的有关章节 webappapis.html#concept-task。
task 源包括:(webappapis.html#generic-task-sources)
此外 setTimeout、setInterval、IndexDB 数据库操作等也是任务源。总结来说,常见的 task 任务有:
2.2 microtask
看规范 webappapis.html#microtask:每一个 eventloop 都有一个 microtask 队列。microtask 会排在 microtask 队列而非 task 队列中。
一般来说,microtask 包括:
现在再来看前面的例子,就很清晰了:
3. microtask 的执行时机
在一些文章中将 requestAnimationFrame 划分为 task,理由是假如你在 requestAnimationFrame 的 callback 中注册了 microtask 任务,你会发现该 microtask 任务会在 requestAnimationFrame 的 callback 结束后立刻执行。
在线示例
结果:
使用 devtool 查看运行情况:(本图引自资料5)
可以看到 requestAnimationFrame 中注册的 microtask 并没有在下一轮 eventloop 的 task 之后执行,而是直接在本轮 eventloop 中紧跟着 requestAnimationFrame 执行了。起初我认为规范明摆着表明 eventloop 由执行 task、执行 microtask、UI render 三部分构成,从属于 render 阶段的 requestAnimationFrame 说是 task 怎么不大合适。但是多检索了一下,发现 W3C 工作组在 2015 年 9 月 22 日的一篇工作笔记《Timing control for script-based animations》 中提到了
animation task source
这一概念,在该文中,确实将 animation frame request callback list 中的 callback 作为 task 处理。另外,在 zone.js 中也将 requestAnimationFrame 划进 macrotask 分类中。但 whatwg 规范中对 requestAnimationFrame callback 未明确出现任何 task 相关字眼,由于 whatwg 和 w3c 的分歧,我对 requestAnimationFrame 是否该划分为 task 存保留意见。我们再研究下 microtask 的执行时机。
eventloop 规范的第 6 步直接表明,task 执行后执行 microtask。我们在 html 规范中进行检索,可以发现,在这些时刻都会 perform a microtask checkpoint:
举例:
在
Calling scripts
的清理阶段,如果 javascript 执行栈为空,也会perform a microtask checkpoint
,即执行 microtask。联系《Tasks, microtasks, queues and schedules》文中的解释:在 callback 执行完,若没有其他 javascript 正在执行中(javascript 执行栈为空),microtask 队列会被处理,与在 task 结束后一样。从这里可知,microtask 虽也是延迟任务,但是它的执行时机『见缝插针,尽可能早』——只要 javascript 执行栈为空,就会执行 microtask。eventloop 中的第 6 步只是这种策略的其中一种场景而已。
深入 requestAnimationFrame 的执行过程也能发现:在执行 animation frame callbacks 时,会唤起 callback(invoke the callback),在唤起 callback 的最后一步,会 clean up after running a callback,此时若满足 javascript 执行栈为空的条件,则执行 microtask。
也就是说一轮 eventloop 中有可能执行多次 microtask。
有一个非常非常有趣的例子可以佐证这个观点,请务必看一下:
我们构建一个嵌套 DOM。为这两个 DOM 绑定相同的 click 回调函数,我们点击内部 DOM 时,由于事件冒泡,内部和外部的 onClick 会依次触发。
在第一个测试中,我们通过点击内部 DOM,让浏览器去派发 click 事件。 在第二个测试中,我们通过 Javascript 触发 click 事件。猜猜输出结果有什么不同?
在线示例A 在线示例B
第一个测试的结果:
第二个测试的结果:
我简单的画下时序图:
第一个测试:
第二个测试:
测试结果很有趣的佐证了『只要 javascript 执行栈为空,就会执行 microtask』的观点。
4. requestAnimationFrame callback 的执行时机
执行 requestAnimationFrame callback 是 UI Render 的其中一步。上文已经提到过并不是每轮 eventloop 都会执行 UI Render。我们仔细看文档:
如果浏览器试图实现 60Hz 的刷新率,那么 UI Render 只需要每秒执行 60 次(每 16.7 ms)。如果浏览器发现『顶层浏览器上下文』无法维持住这个频率,可能会下调到可维持的 30Hz,而不是掉帧。(本规范并不对何时进行 render 做任何规定。)类似的,如果一个顶层浏览器上下文在后台运行,用户代理可能决定将该页面的刷新率降到 4Hz,甚至更低。
由于规范没有做约定,所以浏览器在 render 策略上有充分的自主性。既有可能出现每一轮 eventloop 后都 render 的现象,也有可能出现几十轮 eventloop 都不 render 的情况。
我写了一个简单的测试,在线测试
在 chrome 下比较稳定,requestAnimationFrame callback 始终在第一个 eventloop 时执行,而在 Firefox 下结果就是可变的,多刷新几次会得到不同的结果。这也表明了不同浏览器的 render 策略是不同的。
5. 总结
6. 闲谈 whatwg 与 w3c
你可能发现文中的规范引用多数来自 whatwg,少数来自 w3c。html 规范目前由两个工作组 whatwg 和 w3c 制定,whatwg 制定的叫『HTML Living Standard』,背后是 Opera、Mozilla、Chrome、Safari 等大佬。 w3c 制定的叫『HTML5』,其为经典的万维网联盟,背靠微软,曾在 html 标准制定上拥有绝对话语权二十余年(目前正惨遭 whatwg 嫌弃)。二者时而分歧,时而合作,搞出了两套 HTML 规范。我们在挖掘规范时,需要综合的来看待。
(本文的论点主要依赖于 whatwg 规范)
7. 参考资料