js之DOM深入学习总结

什么是domReady?

html标签和dom节点的区别是什么?

html是一门标记语言,它告诉我们这个页面有什么内容。但行为操作是要通过Dom交互来实现的。我们不能认为只要存在html标签,这个标签就是一个dom了。html标签要通过浏览器解析才能变成dom节点,当我在地址栏中输入一个url的时候,浏览器开始加载页面,我们就能看到内容,在这个过程中,有一个dom节点构建的过程,节点是以树的形式组织的。当页面中的所有html标签都转化为dom节点之后,就叫做dom树构建完毕,我们简称为domReady.其中浏览器是通过渲染引擎将html标签转化为dom节点。渲染引擎的职责是:把请求到的内容显示到浏览器屏幕上。

浏览器渲染引擎的基本渲染流程

页面渲染的基本流程:
解析HTML文档构建DOM树——构建渲染树——渲染树布局——绘制渲染树
详细过程:
第一步:浏览器将获取到的HTML文档解析成DOM树,HTML文档中的每个元素都对应DOM树中的1个节点,根节点就是我们常用的document对象。DOM树里包含了所有HTML标签,包括display:none隐藏的元素,还有用JS动态添加的元素等。
第二步: DOM树和样式结构体(解析样式信息,包括外部的css文件、style标签中的样式)组合后构建render树, 与DOM树的不同在于:render树能识别样式,render 树中每个节点都有自己的样式,而且 render 树不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render 树中。注意visibility:hidden隐藏的元素还是会包含到 render树中的,因为visibility:hidden会影响布局(layout),会占有空间。渲染树由一些包含有各种属性的矩形组成,他们将会按照正确的顺序显示到屏幕上;
第三步:布局渲染树(布局DOM节点),执行布局的过程,将确定每个节点在屏幕上的确切坐标;
第四步:绘制渲染树(绘制DOM节点,即遍历渲染树),使用UI后端层来绘制每个节点

关于更详细的渲染流程,推荐阅读:前端必读:浏览器内部工作原理

window.onload事件是在页面所有的资源都加载完毕后触发的. 如果页面上有大图片等资源响应缓慢, 会导致window.onload事件迟迟无法触发.所以出现了DOM Ready事件. 此事件在DOM文档结构准备完毕后触发, 即在资源加载前触发.

DOMContentLoaded 事件
这个事件在许多Webkit浏览器以及IE9上都可以使用, 此事件会在DOM文档准备好以后触发, 包含在HTML5标准中. 对于支持此事件的浏览器, 直接使用DOMContentLoaded事件是最简单最好的选择.但是IE6,7,8都不支持DOMContentLoaded事件.所以目前所有的hack方法都是为了让IE6,7,8支持DOM Ready事件.

doScroll : 微软的文档指出doScroll必须在DOM主文档准备完毕时才可以正常触发. 所以通过doScroll判断DOM是否准备完毕.
注意:单纯使用readyState属性是无法判断出Dom Ready事件的. interactive状态过早(DOM没有稳定), complete状态过晚(图片加载完毕).

高性能JavaScript之DOM编程

文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档的应用程序接口,用脚本进行DOM操作的代价很昂贵,它是Web应用中最常见的性能瓶颈。有个贴切的比喻,把DOM和JavaScript(这里指ECMScript)各自想象为一个岛屿,它们之间用收费桥梁连接,ECMAScript每次访问DOM,都要途径这座桥,并交纳”过桥费”。访问DOM的次数越多,费用也就越高。因此,推荐的做法是尽量减少过桥的次数,努力待在ECMAScript岛上。

DOM访问与修改

访问DOM元素是有代价的,即前面提到的”过桥费”。修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变化(重排和重绘)。
当然,最坏的情况是在循环中访问或者修改元素,尤其是对HTML元素集合循环操作。
Demo:

<body>
<div id="box">div>
<script type="text/javascript">
var times = 20000;
//测试1
console.time(1);
for(var i=0;i"box").innerHTML+='a';
}
console.timeEnd(1);//1: 5961.652ms
//测试2
console.time(2);
var str = '';
for(var i=0;i'a';//这里用局部变量存储修改中的内容,然后在循环结束后一次性写入
}
document.getElementById("box").innerHTML += str;
console.timeEnd(2);//2: 14.720ms
script>
body>

在所有浏览器中,修改后的版本都运行得更快。
测试代码1的问题在于:每次循环迭代,该元素都会被访问两次:一次读取innerHTML的值,另一次重写它,也就是说,每次循环都在”过桥”!结果显而易见,访问DOM的次数越多,代码的运行速度越慢。因此,减少访问DOM访问的次数,把运算尽量留在ECMAScript这端处理。

重绘和重排

浏览器下载完页面中的所有组件——HTML标记、JavaScript、CSS、图片之后会解析生成两个内部数据结构:

  • DOM树(表示页面结构)
  • 渲染树(表示DOM节点如何显示)

DOM树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DOM元素(disply值为none) 在渲染树中没有对应的节点)。渲染树中的节点被称为”帧”或”盒”,符合CSS模型的定义,理解页面元素为一个具有内边距,外边距,边框和位置的盒子。一旦DOM和渲染树构建完成,浏览器就开始显示(绘制”paint”)页面元素。

当DOM的变化影响了元素的几何属性(宽或高)–比如改变边框宽度或给段落增加文字,导致行数增加–浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排(reflow)。完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘(repaint)

并不是所有的DOM变化都会影响几何属性,比如改变一个元素的背景色并不会影响元素的宽和高,在这种情况下,只会发生一次重绘(不需要重排),因为元素的布局并没有改变。重绘和重排都是代价昂贵的操作,它们会导致Web应用程序的UI反应迟钝,所以,应当尽可能减少这类过程的发生。
特别注意: 重排必将引起重绘,而重绘不一定会引起重排
重排和重绘的代价究竟有多大?我们再回到上面那个例子上,我们发现千倍的时间差并不是由于”过桥”一手造成的,每次”过桥”其实都伴随着重排和重绘,而耗能的绝大部分也正是在这里!
Demo:

<body>
<div id="box1">div>
<div id="box2">div>
<div id="box3">div>
<script type="text/javascript">
var times = 20000;
// 测试1 每次过桥+重排+重绘
console.time(1);
for(var i = 0; i < times; i++) {
  document.getElementById('box1').innerHTML += 'a';
}
console.timeEnd(1);//7226.836ms
// 测试2 只过桥
console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
  var tmp = document.getElementById('box2').innerHTML;//过桥
  str += 'a';
}
document.getElementById('box2').innerHTML = str;
console.timeEnd(2);//57.384ms
// 测试3
console.time(3);
var str1 = '';
for(var i = 0; i < times; i++) {
  str1 += 'a';
}
document.getElementById('box3').innerHTML = str1;
console.timeEnd(3);//3.735ms
script>
body>

从上面这个例子看出:多次访问DOM对于重排和重绘来说,耗时简直不值一提。

重排何时发生?

重排发生会导致重新构造渲染树,以下情况会发生重排:

  • 添加或删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变(包括:外边距、内边距、边框宽度、宽度、高度等属性改变)
  • 内容改变(例如:文本改变或图片被另一个不同尺寸的图片替代)
  • 页面渲染器初始化
  • 浏览器窗口尺寸改变–resize事件发生时

渲染树变化的排队与刷新

由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。浏览器把所有会引起重排、重绘的操作放入一个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的重排、重绘就变成一次重排重绘。
浏览器虽然进行一定优化处理,然而,你可能会(经常是不知不觉)强制刷新队列并要求计划任务立即执行,这样浏览器的优化作用就失效了。获取布局信息的操作会导致队列刷新,比如以下方法:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (currentStyle in IE)

以上属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的”待处理变化”并触发重排以返回正确的值。我们在实际编码中,应该尽可能避免使用上述属性和方法,它们都会刷新渲染队列,即使你是在获取最近未发生改变的或者与最新改变无关的布局信息。

最小化重绘和重排

看下面这个例子:

var el = document.getElementById('myDiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

示例中有三个样式属性被改变,每一个都会影响元素的几何结构。最糟糕的情况下,会导致浏览器触发三次重排。大部分现代浏览器都为此做了优化,只会触发一次重排。如果在上面代码执行时,有其他代码请求布局信息,这会导致请求布局信息,这会导致触发三次重排。而且,这段代码四次访问DOM,可以被优化。一个能够达到同样效果且效率更高的方式是:合并所有的改变然后一次处理,这样只会修改DOM一次

使用cssText属性

修改代码如下:

var el = document.getElementById('myDiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

使用cssText属性会覆盖已经存在的样式信息,如果想保留现有样式,可以把它附加在cssText字符串后面

var el = document.getElementById('myDiv');
el.style.cssText += 'border-;eft: 1px;'

修改CSS的class名称

另一个一次性修改样式的方法是修改CSS的class名称,而不是修改内联样式。这种方法适合于那些不依赖于运行逻辑和计算的情况。

var el = document.getElementById('myDiv');
el.className = 'active';

批量修改DOM

当需要对一个DOM元素进行一系列操作时,可以采用以下步骤:
1、使元素脱离文档流;
2、对元素应用多重改变;
3、把元素带回文档中。
在这个过程中,会触发两次重排:第一步和第三步。如果忽略这两个步骤,那么第二步所产生的任何修改都会触发一次重排。
三种基本方法使DOM脱离文档:
1、隐藏元素,应用修改,重新显示;
2、使用文档片段,在当前DOM之外构建一个子树,再把它拷贝回文档;
3、将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。

文档片段fragment元素的应用

在文档之外创建并更新一个文档片段,然后把它附加到原始列表中。
文档片段是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片段的一个便利的语法特性是当你附加一个片段到节点时,实际上被添加的是该片段的子节点,而不是片段本身。只触发了一次重排,而且只访问了一次实时的DOM。

让元素脱离动画流

用展开/折叠的方式来显示和隐藏部分页面是一种常见的交互模式。它通常包括展开区域的几何动画,并将页面其他部分推向下方。

一般来说,重排只影响渲染树中的一小部分,但也可能影响很大的部分,甚至整个渲染树。浏览器所需要重排的次数越少,应用程序的响应速度就越快。因此当页面顶部的一个动画推移页面整个余下的部分时,会导致一次代价昂贵的大规模重排,让用户感到页面一顿一顿的。渲染树中需要重新计算的节点越多,情况就会越糟。

使用以下步骤可以避免页面中的大部分重排:

  • 使用绝对位置定位页面上的动画元素,将其脱离文档流
  • 让元素动起来。当它扩大时,会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容
  • 当动画结束时恢复定位,从而只会下移一次文档的其他元素

小结

重排和重绘是DOM编程中耗能的主要原因之一,平时涉及DOM编程时可以参考以下几点:

  • 尽量不要在布局信息改变时做查询(会导致渲染队列强制刷新
  • 同一个DOM的多个属性改变可以写在一起(减少DOM访问,同时把强制渲染队列刷新的风险降为0)
  • 如果要批量添加DOM(比如要给ul添加很多li),可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排(文档片段(fragment)元素的应用)
  • 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

推荐一篇相关博文,总结的很好:传送门

你可能感兴趣的:(js)