详解浏览器回流机制
首先,我们需要了解一下重绘和回流的基本定义。
- 回流:是指浏览器需要重新计算元素的几何属性(如位置、尺寸),导致渲染树(Render Tree)的部分或全部重新布局(Layout)的过程
- 重绘:是指当元素的外观样式改变但不影响布局时(如颜色、背景、阴影等),浏览器仅重新绘制(Paint)受影响区域到屏幕的过程
那什么情况下会触发回流?
触发回流操作
- 页面首次渲染
- 浏览器窗口大小发生改变
- 改变元素几何信息
- 增加、删除可见DOM元素
有了基本了解后,我们来看看以下几个例子
案例1
以下代码会触发几次回流?
dom.style.width = '100px'
dom.style.backgroundColor = 'red'
答案:1次。
解读:width = '100px'
属于修改元素几何信息,属于回流操作。而backgroundColor = 'red'
不会影响元素几何大小和布局位置,所以不会触发回流,只会触发重绘。
案例2
以下代码会触发几次回流?
dom.style.width = '100px'
dom.style.marginLeft = '100px'
dom.style.height = '100px'
答案:还是1次
有人会说,width
、marginLeft
、height
不都影响页面布局吗?按理说不是应该触发3次回流吗?
这里涉及到一个浏览器的优化机制
:当我们操作dom的样式时,浏览器并不是立即放入主线程中执行,而是先放入到渲染队列中,等主线程空闲时再一起执行,这样可以减少不必要的回流。
比如上面代码,浏览器会分别创建3个渲染任务(dom.style.width = '100px'、dom.style.marginLeft = '100px'、dom.style.height = '100px')依次推入到渲染队列,然后浏览器空闲时,一次性执行,所以回流只触发一次
案例3
以下代码会触发几次回流?
dom.style.marginLeft = '100px'
dom.style.width = '100px'
dom.style.height = dom.offsetWidth + 10 + 'px'
dom.style.padding = dom.offsetWidth + 10 + 'px'
答案:3次
首先,前两行代码执行后,dom.style.marginLeft = '100px'
和dom.style.width = '100px'
会依次放在渲染队列中, 当第三行代码读取dom.offsetWidth
值时,会触发渲染队列刷新,此时浏览器会强制执行渲染队列中的所有任务,并清空队列,这是第一次回流。 接着dom.style.height = dom.offsetWidth + 10 + 'px'
这段代码会被放入渲染队列中,当第四行代码再次读取dom.offsetWidth
时,又会触发渲染队列刷新,执行dom.style.height = dom.offsetWidth + 10 + 'px'
代码,并触发第二次回流。 最后dom.style.padding = dom.offsetWidth + 10 + 'px'
放入渲染队列,等浏览器空闲时,自动执行,并触发第三次回流。
这里涉及一个新的知识,什么时候获取布局信息,会导致渲染队列刷新?
回答:访问以下方法时:
- offsetTop、offsetLeft、offsetWidth、offsetHeight
- scrollTop、scrollLeft、scrollWidth、scrollHeight
- clientTop、clientLeft、clientWidth、clientHeight
- getComputedStyle()
- getBoundingClientRect()
- 获取窗口尺寸、Range尺寸等。更多方法见文档
案例4
以下代码,当点击按钮后,会触发几次回流?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.root {
width: 50px;
height: 50px;
background-color: red;
}
</style>
</head>
<body>
<div class="root">哈哈</div>
<button>Click me</button>
<script>
const btn = document.querySelector('button')
function handler() {
const dom = document.querySelector('.root')
dom.style.top = '10px'
dom.style.width = dom.offsetWidth + 10 + 'px'
dom.style.width = 20 + 'px'
}
btn.onclick = handler
</script>
</body>
</html>
答案:1次
那如果再上面代码基础上,加上一行样式呢?比如
.root {
width: 50px;
height: 50px;
background-color: red;
position: absolute; // 新增样式
}
重新点击按钮后,会触发几次回流?
答案:2次
为什么js代码没变,只改样式,反而会多触发一次回流呢?
我们来分析下流程: 首先dom.style.top = '10px'
先放入渲染队列,然后第二行代码会先读取dom.offsetWidth
属性,强制刷新渲染队列。在这里需要特别注意,在position: absolute;
样式没有时,修改top
是不影响页面布局的,所以不会触发回流,但是当我们加上position: absolute;
时,修改top
会影响页面布局,所以会触发回流。 接着dom.style.width = dom.offsetWidth + 10 + 'px'
放入渲染队列, 然后dom.style.width = 20 + 'px'
也放入渲染队列, 最后浏览器空闲时,自动执行渲染队列中的所有任务,并清空队列,触发一次回流。
所以,当我们加上position: absolute;
时,总共触发2次回流。没有position: absolute;
时,只会触发1次回流。