####本文编译自Rendering: repaint, reflow/relayout, restyle 所有权利保留为原作者所有
##渲染: 重绘,回流/重布局,重设样式 ###渲染过程
不同的浏览器实现不同,但下面图表所示过程基本都足以表示各个浏览器一旦下载完毕代码,接下来做了些什么。
-
浏览器解析HTML源代码(一堆标签),然后构建一棵 DOM树 ——一种数据表现方式,每一个HTML标签都有一个对应的节点,标签间的文本块则是对应的文本节点。DOM树的根节点是documentElement(也就是HTML标签)
-
浏览器解析CSS代码,一大堆hacks和-moz,-webkit和其他它不能理解的代码会被它直接抛弃, 把剩下的解析出来。样式信息层叠:基础样式是UA默认,然后用户默认,网页样式,导入的样式(imported),行内样式,然后最后是HTML标签里带着的样式。
-
好玩的部分来了——构建一棵 渲染树(Render Tree)。长的有点像DOM树,但并不完全一致。渲染树知道样式,因此如果你将一个div display:none,那么它就不会被呈现在渲染树中。另一方面,有些DOM元素可能被渲染树多个节点表示——例如一个p元素中的每一行文本都需要一个单独的渲染树节点。渲染树的一个节点被称为一个帧(frame),或者盒子(box,和CSS中的盒模型对应)。这些节点上都有css盒属性——width,height,border,margin等等
-
渲染树构建完毕,浏览器就可以 绘制(paint/draw) 渲染树的节点到屏幕上了
###树 各种树 看个例子。
HTML source:
<title>Beautiful page</title>Once upon a time there was a looong paragraph...
Secret message...
该文档生成的DOM树基本给每一个标签对应了一个节点,给节点间的每一段文本都对应了一个文本节点(为简便起见,此处忽视掉也是文本节点的空白符)。
documentElement (html)
head
title
body
p
[text node]
div
[text node]
div
img
...
渲染树是DOM树的可见部分。它会少一些东西——头部和隐藏起来的div,但是它会为多行文本创建额外的节点(又称frame或者boxes)
root (RenderView)
body
p
line 1
line 2
line 3
...
div
img
...
渲染树的根节点是包含所有其他元素的frame(the box)。你可以把它想象成是浏览器窗口里面,就是页面可以占用的受控区域。严格来说,Webkit称这一根节点为RenderView,和CSS初始包含块(initial containing block)相对应,基本上来说就是从page (0,0)一直到(window.innerWidth, window.innerHeight)的方形视窗口(viewport)。
想要确定一个元素具体如何呈现在屏幕上,需要从根部进行递归遍历,走遍渲染树。
###Repaints and reflows 重绘和回流
一般总至少会有一个初始布局和绘制(除非你喜欢空白页面)。在那之后,任何会导致重建渲染树的信息输入都可能引起下面其中一项或全部:
-
渲染树的部分(或整个渲染树)需要被重新验证,节点尺寸重新计算。这被称为回流(reflow)、布局(layout 或者layouting)。有一点要注意:回流至少会发生一次——即初始化页面布局的时候
-
屏幕部分内容需要被更新,要么是因为节点几何属性有变化,要么是因为样式变化,例如背景色改变。这一更新被称为重绘(repaint,redraw)
回流和重绘的代价可能很昂贵。有害于用户体验,UI看起来也反应迟缓。
####什么可能触发回流和重绘
任何改变了用于构建渲染树的信息的行为都可以引起一次回流或者重绘,例如:
- 添加、移除、更新DOM节点
- 使用display:none (重绘和回流) 或者visibility: hidden (只重绘,因为几何属性未变化)隐藏节点
- 在页面上移动DOM节点,或者做动画
- 添加样式表或者调整样式
- 缩放窗口、改变字体大小、滚动窗口(哦这个很要命)等用户行为
例子:
var bstyle = document.body.style; // cache
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // another reflow and a repaint
bstyle.color = "blue"; // repaint only, no dimensions changed
bstyle.backgroundColor = "#fad"; // repaint
bstyle.fontSize = "2em"; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));
有些回流代价比较高。想一下渲染树——如果你改变的是body的一个直系后代,或许你不会需要跑一遍其他节点。但是如果你做了个动画,在页面头部展开了一个div,它把整个页面的其余部分都给往下推了推——这代价就高了。
####浏览器很聪明
既然渲染树改变导致的回流和重绘代价这么高,浏览器自然致力于减轻其负面影响。一种方法就是不做这个事。或者至少不在现在做。浏览器会起一个队列存下所有你的脚本的改变请求,然后攒起来一并执行。这样子需要几次回流的多个操作便可以被组合起来,只需要计算一次回流。浏览器可以存住变化,然后间隔一定时间或者队列积累到一定数量后后执行并清空队列。
但有的时候,脚本很有可能不让浏览器这么做,并令其立马冲刷队列执行所有变化。这发生在你请求样式信息的时候,例如:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- getComputedStyle(), or currentStyle in IE
以上这些都会要求节点的样式信息,而在你这么做的时候,浏览器不得不给你最新的数据。为了获取最新数据,它必须得立即执行所有变化,冲刷掉队列,咬紧牙关地执行回流。
例如,在一系列连续操作中设置和获取样式就是个很烂的主意:
// no-no!
el.style.left = el.offsetLeft + 10 + "px";
####最小化重绘和回流
减少回流重绘对用户体验的负面影响的策略,简单来说就是尽可能减少重绘和回流,减少对样式信息的请求,让浏览器可以优化回流。这要怎么做呢?
不要一条条地改变单独的样式值。最好最具有可维护性的办法是更改class而不是样式。但这是假定是静态样式的情况下。如果样式是动态的,那么就直接编辑cssText属性,而不要去为了每一个小变化去碰元素和它单独的样式属性。
// 糟糕的做法
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// 好一点
el.className += " theclassname";
//如果需要动态改变样式的取值。。。
// 好做法
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
将DOM变化打包,并‘离线’操作。
离线操作的意思是不要在DOM树里操作,你可以: 使用一个documentFrament来保存临时的变化; 把你想要改变的节点复制出来,在复制品上进行操作,然后再用改完的复制品替换原节点。
把该元素用 display: none藏起来(花费一个回流和重绘),在上面做100个改变,再把它display出来(又一个回流和重绘)。这样你只用2词次回流避免了原本可能发生的一百次。
不要频繁请求样式计算值。如果你需要使用计算值,取出来,保存在本地的变量里,用这个本地复制品操作。重新访问上面的no-no例子:
// no-no! 糟透了哎哟喂
for(big; loop; here) {
el.style.left = el.offsetLeft + 10 + "px";
el.style.top = el.offsetTop + 10 + "px";
}
// better 好点了
var left = el.offsetLeft,
top = el.offsetTop
esty = el.style;
for(big; loop; here) {
left += 10;
top += 10;
esty.left = left + "px";
esty.top = top + "px";
}
总的来说,想着渲染树,然后思考自己做的变化会需要它重新计算多大工作量。例如使用绝对定位把元素变成body的直系子元素,这样给它做动画的时候就不会影响太多其他节点了。当你的元素跑到某一块上面的时候,那一块地方的其他节点可能需要重绘,但它们就不用回流了。
Written with StackEdit.