写在前面
重绘和重排也称重绘和回流(Repaint & Reflow)。我在网上查阅了许多资料基本都是千篇一律,很难将它说清楚,而且还有一些说法是错误的。因此本文将离开纯理论,从实践中来分析到底什么是重绘和重排
先提两个问题
这里会闪一下吗,如果会,为什么,如果不会,请问怎么能让他闪一下
function appendDataToElement() {
ele.style.width = '500px';
// ele.style.transition = 'width 20s';
document.body.offsetHeight;
ele.style.width = '2000px';
// ele.style.transition = 'width 10s';
document.body.offsetHeight;
ele.style.transition = 'width 5s';
ele.style.width = '200px';
}
var ele = document.getElementById('list');
setTimeout(appendDataToElement, 1000)
请问这个元素会怎么运动,如果我将注释代码放开,又会怎么运动
重绘和重排的定义
我谈下我对重绘和重排的理解:
重绘和重排离不开浏览器的event loop,其主要分为任务队列和渲染任务,如下图:
主线程不停的从任务队列和渲染任务中取出任务执行。其中任务队列和渲染任务是互斥的、执行任务队列中的JS任务时是不能执行渲染任务的(这句话是不太准确的,后面会用chrome火焰图来说明)。
渲染任务一般就分为3个,图中的S表示Recalculate Style,L表示Layout, P表示Paint。
实际上,通过火焰图发现渲染任务还有两种:Pre-Paint,Composite Layers,是否属于渲染任务以及他们的作用,欢迎大家指出。
我们所说的重绘就是S和 P 两个任务,而重排就是S, L,和 P 三个任务。这里我需要说明下:L是依赖于S的,有L任务必须有S任务。而P任务和前面两者不一定是同步的,也就是说可能发生S或S和L、而不发生P。如果不发生P,页面就不会刷新,也就不会出现闪烁的效果,就会是一帧。因此我理解的如果不发生P就不算重绘或者重排。
到这里,重绘和重排的定义就结束了。下面我们来讨论下上面提出的两个问题:
问题1: 明显不会闪烁,因为JS任务和渲染任务互斥,所以永远不会闪烁。至于如果让其闪烁,可以考虑用requestAnimationFrame。另外请大家思考下是否可以用setTimeout和offsetHeight实现。
问题2比较复杂,我们稍后再分析,先来看看其他文章的说法:
另外一篇是腾讯大佬写的文章,我也摘了一段分析。
对于上面两种说法,我都抱有质疑的态度。我的结论是:
任何JS操作都不会触发完整的重绘或者回流,放心大胆的用,除了某些JS操作会触发不完整的重绘或回流。例如offsetWidth某些情况下会触发S任务,某些情况下会触发S和L任务,但是任何JS操作都无法触发P任务,只有依赖浏览器自己去触发。
下面我通过问题2的火焰图来说明:
可以看到两次offsetHeight都触发了S和L任务、并没有触发P任务,因此最后的动画会从最后一次L任务渐变到最终的形态。也就是宽度从2000px渐变到200px。
如果我们打开注释部分代码会怎么样:
相比于上图,第二次的offsetHeight只触发了S任务,所以会被过滤掉。宽度会从500px渐变到200px。
这里我们设置的是会触发L任务的属性,设置只触发S任务的属性也是一样的,只会记录住最后一次触发的状态,渐变到最终的状态。并且,如果我们不设置transition属性,将不会看到任务过渡效果,宽度不会从500px => 2000px => 200px,只会从最初的宽度瞬间到200px,因为我们没有触发paint。
因此与其说任务队列和渲染任务是互斥的,不如说任务队列和P任务是互斥的,JS任务后面可能会穿插S任务或者L任务,但永远不会触发P任务。
最后
其实很多代码的效果是无法预测的,因为S,L, P三个任务是黑盒,我们只能操作任务队列部分。不过有了火焰图之后,我们就可以分析其中的原理了,也希望大家能熟练掌握火焰图的使用。
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。