文章目录
首先提一个我之前积累过得问题
浏览器是如何把URL 变成一个屏幕上显示的网页的?
- 浏览器首先使用http 协议或者https 协议,向服务器请求页面
- 把请求回来的HTML 代码经过解析,构建成DOM 树
- 计算dom 树上的css 属性,得到CSSDOM树
- 最后根据css属性对元素逐个进行渲染,得到内存中的位图(点阵图,是由像素的单个点组成的,这些点可以进行不同的排列和染色构成图样)【结合DOM树和CSSDOM树,得到渲染树】
- 一个可选的步骤是对位图进行合成,这会极大的增加后续绘制的速度【将渲染树的是所有节点进行位图合成,生成布局flow】
- 合成之后,再绘制【paint】到界面上
如果上图不能理解的话,就看下面这个图
需要知道的是:
从http请求回来开始,这个过程就并非一般想象中的一步做完再做下一步,而是一条流水线
从http请求回来,就产生了流式的数据,后续的DOM 树构建,css计算,渲染,合成,绘制,都是尽可能的流式的处理前一步的产出;即不需要等到上一步完全结束,就直接开始上一步的输出,这样我们在浏览网页时,才会看到逐步出现的页面。
1. 构建Dom 树
我觉得这个部分比较像编译原理(通过词法分析,语法分析进行构建)
这颗构建的Dom 树,实际上信息不是全的,只有节点和属性,不包括任何的样式和信息
构建Dom 的过程是:从父到子,从先到后,一个一个节点构造,并且挂载到Dom 树上
2. 把css 属性规则应用到节点上,给Dom树添加上css 属性
在构建Dom 树时,同步计算了css 属性
我们依次拿到上一步构建好的元素,去检查他匹配了哪些规则,再根据规则的优先级,进行覆盖和调整
选择器的出现顺序,必定跟构建Dom 树的顺序一致。这是css设计的原则,即保证选择器在Dom 树构建到当前节点时,已经可以准确的判断是否可以匹配,而不需要后续节点的信息
3. 浏览器确定元素的位置(排版)
可分为
- 文字排版
- 盒排版
- 正常流排版
- 绝对定位排版
- 浮动元素排版
- 其他排版
- flex 排版
4. 根据样式信息和大小信息,为元素在内存中进行渲染,并绘制到响应的位置
渲染(render)
可分为
- 图形
- 文字
渲染过程(特指我这里讲的)是不会把子元素绘制到渲染的位图上去,这样,当父子元素的相对位置发生变化时,可以保证渲染的结果能够最大程度化被缓存,减少重新渲染
合成(compositing)
上一小节中讲到,渲染过程不会把子元素渲染到位图上面,合成的过程:就是为一些元素创建一个“合成后的位图”(合成层),把一部分子元素渲染到合成的位图上面
目前主流浏览器一般根据position transform 等属性来决定合成策略,来“猜测”这些元素在未来可能发生变化
绘制
绘制是把“位图最终绘制到屏幕上,变成肉眼可见的图像”
我们把任何位图合成到这个“最终位图”的操作称为绘制
一般来说浏览器并不需要用代码来处理这个过程,浏览器只需要把最终的要显示的位图交给操作系统即可
在上面两个小节中,已经得到了每个元素的位图,并且对他们部分进行了合成,绘制过程就是按照z-index
把他们一次绘制到屏幕上
重绘和重排
- 重绘:某些元素的外观被改变,例如:元素的填充颜色
- 重排:重新生成布局,重新排列元素
仔细理解概念:单单改变元素的外观,肯定不会引起网页重新生成布局。但是浏览器完成重排之后,重绘将会受到重排的影响。
"重绘"不一定会出现"重排","重排"必然会出现"重绘"
重排(reflow)
DOM对象的位置和尺寸大小改变时,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
回流:
回流就好比向河里(文档流)扔了一块石头(dom变化),激起涟漪,然后引起周边水流受到波及,所以叫做回流
常见引起重排的方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排
- 添加或删除可见的Dom 元素
- 元素尺寸改变–边距、填充、边框、宽度和高度
- 内容变化,比如用户在input框中输入文字
- 浏览器窗口改变–resize事件发生时
- 计算offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
重排影响的范围
由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种:
- 全局范围:从根节点
html
开始对整个渲染树进行重新布局。 - 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界。
重绘(repaint)
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
常见引起重绘的属性:
浏览器的渲染队列
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
根据我们上文的定义,这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性,实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制:
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
强制刷新队列:
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);
这段代码会触发4次重排+重绘,因为在console中你请求的这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。
因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘。
强制刷新队列的style样式请求:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle(), 或者 IE的 currentStyle
我们在开发中,应该谨慎的使用这些style请求,注意上下文关系,避免一行代码一个重排,这对性能是个巨大的消耗
减少重绘重排的操作
1. 分离读写操作
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);
还是上面触发4次重排+重绘的代码,这次只触发了一次重排:
在第一个console的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。剩下的console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。
2. 样式集中改变(使用className)
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
虽然现在大部分浏览器有渲染队列优化,不排除有些浏览器以及老版本的浏览器效率仍然低下:
建议通过改变class或者csstext属性集中改变样式
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// good
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
3. 缓存布局信息(使用变量储存计算样式)
// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
4. 离线改变dom
- dom 改变之前可设置先隐藏
在要操作dom之前,通过display隐藏dom,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘。
dom.display = 'none'
// 修改dom样式
dom.display = 'block'
- 通过使用DocumentFragment创建一个dom碎片,在它上面批量操作dom,操作完成之后,再添加到文档中,这样只会触发一次重排。
- 复制节点,在副本上工作,然后替换它!