event loop 中的 Update the rendering(更新渲染)
这是 event loop 中很重要部分,在这篇文章处理流程(processing model)中第 3 步会进行 Update the rendering(更新渲染),规范允许浏览器自己选择是否更新视图。也就是说可能不是每轮事件循环都去更新视图,只在有必要的时候才更新视图。
渲染的基本流程:
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树, 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局,以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
Note: 可以看到渲染树的一个重要组成部分是 CSSOM 树,绘制会等待 css 样式全部加载完成才进行,所以 css 样式加载的快慢是首屏呈现快慢的关键点。
下面讨论一下渲染的时机。规范定义在一次循环中,Update the rendering 会在 Microtasks: Perform a microtask checkpoint 后运行。
渲染时机
以下的例子中,用 chrome 的 Developer tools 的 Timeline 查看各部分运行的时间点。当我们点击这个 div 的时候,截取了部分时间线。
黄色部分是脚本运行,紫色部分是更新 render 树、计算布局,绿色部分是绘制。
绿色和紫色部分可以认为是 Update the rendering。
例子 1
1 | <div id="con">this is con</div> |
在这一轮事件循环中,setTimeout1 是作为 task 运行的,可以看到 paint 确实是在 task 运行完后才进行的。
例子 2
现在换成一个 microtask 任务,看看有什么变化
1 | <div id="con">this is con</div> |
和上一个例子很像,不同的是这一轮事件循环的 task 是 click 的回调函数,Promise1 则是 microtask,paint 同样是在他们之后完成。
标准就是那么定义的,答案似乎显而易见,我们把例子变得稍微复杂一些。
例子 3
1 | <div id="con">this is con</div> |
经过多次测试,执行和渲染顺序可能会出现上图的两种情况,根据 timeline 可以看出,图 1 中 setTimeout 分别执行并且浏览器绘制了两次,图二中 setTimeout1 和 setTimeout2 中间并没有绘制, 而是最后绘制了一次,也基本符合规范,但是需要验证这两次 setTimeout 是否在两次 task 中。
例子 4
在两个 setTimeout 中增加 microtask。再次确认 setTimeout 在两个 task 中
1 | <div id="con">this is con</div> |
从 run microtasks 中可以看出来,setTimeout1、setTimeout2 是运行在两次 event loop 中
例子 5
将时间间隔加大一些。
1 | <div id="con">this is con</div> |
当间隔增大,大部分时候可以肉眼从看到先变成 0,再变成 1 的过程。但是有时也是会合并起来,只 paint 一次
通过 timeline,可以看到 setTimeout1 后接着 paint,后执行了 setTimeout2 后也有 paint
例子 6
我们在同一时间执行多个 setTimeout 来模拟执行间隔很短的 task。
1 | <div id="con">this is con</div> |
图一中总共 paint 了一次
图二中一共 paint 了两次,所以多次 task 的间隔很短,仍会进行绘制。
例子 7
有说法是一轮 event loop 执行的 microtask 有数量限制,多余的 microtask 会放到下一轮执行。下面例子将 microtask 的数量增加到 25000。
1 | <div id="con">this is con</div> |
可以看到脚本的运行耗费大量的时间,并且阻塞了渲染。
我们看 setTimeout2 的运行情况
可以看到 setTimeout2 这轮 event loop 没有 run microtasks,microtasks 在 setTimeout1 被全部执行完了。
25000 个 microtasks 不能说明 event loop 对 microtasks 数量没有限制,有可能这个限制数很高,远超 25000,但日常使用基本不会使用那么多了。
对 microtasks 增加数量限制,一个很大的作用是防止脚本运行时间过长,阻塞渲染。
例子 8
使用 requestAnimationFrame。
1 | <div id="con">this is con</div> |
看下总体的 timeline
单看某一个 requestAnimationFrame
- 可以看出 requestAnimationFrame 是在更新渲染阶段的执行的,其执行顺序早于 paint,并且不会被合并执行,非常适合做动画
- 在 requestAnimationFrame 回调内有新的 microtasks 进入时,也会执行完所有的 microtasks 才会进入到渲染阶段
例子 9
验证 postMessage 是否是 task
1 | <script> |
执行顺序:
1 | sync |
第一个黄块是 setTimeout1,第二个是 onmessage1,第三个是 promise1,第四个是 setTimeout2。显而易见,postMessage 属于 task。
总结
结合规范和以上验证案例可以得出一些结论:
- event loop 的大致循环过程,可以用下边的图表示:
- 在一轮 event loop 中多次修改同一 dom,只有最后一次会进行绘制。
- 例 3-例 6 这几个结果是非常不可控的,如果这两个 Task 之间正好遇到了浏览器认定的渲染机会,那么它会重绘,否则就不会。
- 渲染更新(Update the rendering)会在 event loop 中的 tasks 和 microtasks 完成后进行,但并不是每轮 event loop 都会更新渲染,这里有一个 rendering opportunity 的概念,判断是否需要渲染,这取决于是否修改了 dom 和浏览器觉得是否有必要在此时立即将新状态呈现给用户,也要要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定。通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行)
- 事件循环不一定每轮都伴随着重渲染,但是如果有微任务,一定会伴随着微任务执行。
- 如果希望在每轮 event loop 都即时呈现变动,可以使用 requestAnimationFrame。