DOM 是(Document Object Model)一个与语言无关的、用来操作 XML 和 html 文档的应用程序接口(Application Program Interface)。 尽管 DOM 与语言无关,但是在浏览器中的接口却是用 JavaScript 来实现的。
浏览器通常会把 js 和 DOM 分开来分别独立实现。
举个栗子冷知识,在 IE 中,js 的实现名为 JScript,位于 jscript.dll 文件中;DOM 的实现则存在另一个库中,名为 mshtml.dll(Trident)。
Chrome 中的 DOM 实现为 webkit 中的 webCore,但 js 引擎是 Google 自己研发的 V8。
Firefox 中的 js 引擎是 SpiderMonkey,渲染引擎(DOM)则是 Gecko。
前面的小知识中说过,浏览器把实现页面渲染的部分和解析 js 的部分分开来实现,既然是分开的,一旦两者需要产生连接,就要付出代价。
两个例子:
因此,推荐的做法是:尽可能的减少过桥的次数,努力待在 ECMAScript 岛上。
前面说到访问 DOM 需要交纳 "过桥费",而修改 DOM 元素则代价更为昂贵,因为它会导致浏览器重新计算页面的几何变化。 来看一段代码:
- function innerHTMLLoop(){
- for (var count = 0; count < 15000; count++){
- document.getElementById('text').innerHTML += 'dom';
- }
- }
这段代码,每次循环会访问两次特定的元素:第一次读取这个元素的 innerHTML 属性,第二次重写它。
看清楚了这一点,不难得到一个效率更高的版本:
- function innerHTMLLoop2(){
- var content = '';
- for (var count = 0; count < 15000; count++){
- content += 'dom';
- }
- document.getElementById('text').innerHTML += content;
- }
用一个局部变量包层每次更新后的内容,等待循环结束后,一次性的写入页面(尽可能的把更多的工作交给 js 的部分来做)。
根据统计,在所有的浏览器中,修改后的版本都运行的更快(优化幅度最明显的是 IE8,使用后者比使用前者快 273 倍)。
HTML 元素集合是包含了 DOM 节点引用的类数组对象。
可以用以下方法或属性得到一个 HTML 元素集合:
HTML 元素集合处于一种 "实时的状态",这意味着当底层文档对象更新时,它也会自动更新,也就是说,HTML 元素集合与底层的文档对象之间保持的连接。正因如此,每当你想从 HTML 元素集合中获取一些信息时,都会产生一次查询操作,这正是低效之源。
- //这是一个死循环 //不管你信不信,反正我是信了 var alldivs = document.getElementsByTagName('div'); for (var i = 0; i < alldivs.length; i++){
- document.body.appendChild(document.createElement('div'));
- }
乍一看,这段代码只是单纯的把页面中的 div 数量翻倍:遍历所有的 div,每次创建一个新的 div 并创建到添加到 body 中。
但事实上,这是一个死循环:因为循环的退出条件 alldivs.length 在每一次循环结束后都会增加,因为这个 HTML 元素集合反映的是底层文档元素的实时状态。
接下来,我们通过这段代码,对一个 HTML 元素集合做一些处理:
- function toArray(coll){
- for (var i = 0, a = [], len = coll.lengthl i < len; i++){
- a[i] = coll[i];
- }
- return a;
- }
- //将一个HTML元素集合拷贝到一个数组中 var coll = document.getElementsByTagName('div'); var arr = toArray(coll);
现在比较以下两个函数:
- function loopCollection(){
- for (var count = 0; count < coll.length; count++){
- //processing...
- }
- }
- function loopCopiedArray(){
- for (var count = 0; count < arr.length; count++){
- //processing...
- }
- }
在 IE6 中,后者比前者快 114 倍;IE7 中 119 倍;IE8 中 79 倍...
所以,在相同的内容和数量下,遍历一个数组的速度明显快于遍历一个 HTML 元素集合。
由于在每一次迭代循环中,读取元素集合的 length 属性会引发集合进行更新,这在所有的浏览器中都有明显的性能问题,所以你也可以这么干:
- function loopCacheLengthCollection(){
- var coll = document.getElementsByTagName('div'),
- len = coll.length;
- for (var count = 0; count < len; count++){
- //processing...
- }
- }
这个函数和上面的 loopCopiedArray() 一样快。
一般来说,对于任何类型的 DOM 访问,当同一个 DOM 属性或者方法需要被多次访问时,最好使用一个局部变量缓存此成员。当遍历一个集合时,首要优化原则是把集合存储在局部变量中,并把 length 缓存在循环外部,然后使用局部变量访问这些需要多次访问的元素。
一个栗子,在循环之中访问每个元素的三个属性。
- function collectionGlobal(){
- var coll = document.getElementsByTagName('div'),
- len = coll.length,
- name = '';
- for (var count = 0; count < len; count++){
- name = document.getElementsByTagName('div')[count].nodeName;
- name = document.getElementsByTagName('div')[count].nodeType;
- name = document.getElementsByTagName('div')[count].tagName;
- //我的天不会有人真的这么写吧...
- }
- return name;
- }
上面这段代码,大家不要当真... 正常人肯定是写不出来的... 这里是为了对比一下,所以把这种最慢的情况写给大家看。
接下来,是一个稍微优化了的版本:
- function collectionLocal(){
- var coll = document.getElementsByTagName('div'),
- len = coll.length,
- name = '';
- for (var count = 0; count < length; count++){
- name = coll[count].nodeName;
- name = coll[count].nodeType;
- name = coll[count].tagName;
- }
- return name;
- }
这次就看起来正常很多了,最后是这次优化之旅的最终版本:
- function collectionNodesLocal(){
- var coll = document.getElementsByTagName('div'),
- len = coll.length,
- name = '',
- ele = null;
- for (var count = 0; count < len; count++){
- ele = coll[count];
- name = ele.nodeName;
- name = ele.nodeType;
- name = ele.tagName;
- }
- return name;
- }
通常你需要从某一个 DOM 元素开始,操作周围的元素,或者递归查找所有的子节点。
考虑下面两个等价的栗子:
- //1 function testNextSibling(){
- var el = document.getElementById('mydiv'),
- ch = el.firstChild,
- name = '';
- do {
- name = ch.nodeName;
- } while (ch = ch.nextSibling);
- return name;
- }
- //2 function testChildNodes(){
- var el = document.getElementById('mydiv'),
- ch = el.childNodes,
- len = ch.length,
- //childNodes是一个元素集合,因此在循环中主席缓存length属性以避免迭代更新
- name = '';
- for (var count = 0; count < len; count++){
- name = ch[count].nodeName;
- }
- return name;
- }
在不同浏览器中,两种方法的运行时间几乎相等。但在老版本的 IE 浏览器中,nextSibling 的性能比 childNodes 更好一些。
我们知道,DOM 节点有以下五种分类:
诸如 childNodes、firstChild、nextSibling 这些 DOM 属性是不区分元素节点和其他类型的节点的,但往往我们只需要访问元素节点,此时需要做一些过滤的工作。事实上,这些类型检查的过程都是不必要的 DOM 操作。
许多现代浏览器提供的 API 只返回元素节点,如果可用的话推荐直接只用这些 API,因为它们的执行效率比自己在 js 中过滤的效率要高。
使用这些新的 API,可以直接获取到元素节点,也正是因此,其速度也更快。
有时候为了得到需要的元素列表,开发人员不得不组合调用 getElementById、getElementsByTagName,并遍历返回的节点,但这种繁密的过程效率低下。
最新的浏览器提供了一个传递参数为 CSS 选择器的名为 querySelectorAll() 的原生 DOM 方法。这种方式自然比使用 js 和 DOM 来遍历查找元素要快的多。
比如,
- var elements = document.querySelectorAll('#menu a');
这一段代码,返回的是一个 NodeList————包含着匹配节点的类数组对象。与之前不同的是,这个方法不会返回 HTML 元素集合,因此返回的节点不会对应实时的文档结构,也避免了之前由于 HTML 集合引起的性能(潜在逻辑)问题。
如果不使用 querySelectorAll(),我们需要这样写:
- var elements = document.getElementById('menu').getElementsByTagName('a');
不仅写起来更麻烦了,更要注意的是,此时的 elements 是一个 HTML 元素集合,所以还需要把它 copy 到数组中,才能得到一个与前者相似的静态列表。
还有一个 querySelector() 方法,用来获取第一个匹配的节点。
浏览器用来显示页面的所有 "组件",有:HTML 标签、js、css、图片——之后会解析并生成两个内部的数据结构:
DOM 树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点。
渲染树中的节点被称为 "帧(frames)" 或 "盒(boxes)",符合 css 盒模型的定义,理解页面元素为一个具有 padding、margin、borders 和 position 的盒子。
一旦渲染树构建完成,浏览器就开始显示页面元素,这个过程称为绘制(paint)。
当 DOM 的变化影响了元素的几何属性(宽、高)——比如改变改变了边框的宽度或者给一个段落增加一些文字导致其行数的增加——浏览器就需要重新计算元素的几何属性,同样,页面中其他元素的几何属性和位置也会因此受到影响。
浏览器会使渲染树中收到影响的部分消失,重新构建渲染树,这个过程称为 "重排 (reflow)"。重排完成之后,浏览器会重新将受到影响的部分绘制到浏览器中,这个过程称之为 "重绘 (repaint)"。
如果改变的不是元素的几何属性,如:改变元素的背景颜色,不会发生重排,只会发生一次重绘,因为元素的布局并没有改变。
不管是重绘还是重排,都是代价昂贵的操作,它们会导致 web 应用程序的 UI 反应迟钝,应当尽可能的减少这类过程的发生。
一个栗子:
- var el = document.getElementById('mydiv');
- el.style.borderLeft = '1px';
- el.style.borderRight = '2px';
- el.style.padding = '5px';
示例中,元素的三个样式被改变,而且每一个都会影响元素的几何结构。在最糟糕的情况下,这段代码会触发三次重排(大部分现代浏览器为此做了优化,只会触发一次重排)。从另一个角度看,这段代码四次访问 DOM,可以被优化。
- var el = document.getElementById('mydiv'); //思路:合并所有改变然后一次性处理 //method_1:使用cssText属性 el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
- //method_2:修改类名 el.className = 'anotherClass';
当你需要对 DOM 元素进行一系列操作的时候,不妨按照如下步骤:
上面的这一套组合拳中,第一步和第三部分别会触发一次重排。但是如果你忽略了这两个步骤,那么在第二步所产生的任何修改都会触发一次重排。
在此安利三种可以使 DOM 元素脱离文档流的方法:
一般情况下,重排只影响渲染树中的一小部分,但也可能影响很大的一部分,甚至是整个渲染树。
浏览器所需的重排次数越少,应用程序的响应速度也就越快。
想象这样一种情况,页面的底部有一个动画,会推移页面整个余下的部分,这将是一次代价昂贵的大规模重排!用户也势必会感觉到页面一卡一卡的。
因此,使用以下步骤可以避免页面中的大部分重排:
从 IE7 开始,IE 允许在任何元素上使用: hover 这个 css 选择器。
然而,如果你有大量元素使用了: hover,你会发现,贼喇慢!
这一个优化手段也是在前端求职面试中的高频题目。
当页面中有大量的元素,并且这些元素都需要绑定事件处理器。
每绑定一个事件处理器都是有代价的,要么加重了页面负担,要么增加了运行期的执行时间。再者,事件绑定会占用处理时间,而且浏览器需要跟踪每个事件处理器,这也会占用更多的内存。还有一种情况就是,当这些工作结束时,这些事件处理器中的绝大多数都是不再需要的(并不是 100% 的按钮或链接都会被用户点击),因此有很多工作是没有必要的。
事件委托的原理很简单——事件逐层冒泡并能被父级元素捕获。
使用事件委托,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有事件。
有以下几点需要注意:
访问和操作 DOM 需要穿越连接 ECMAScript 和 DOM 两个岛屿之间的桥梁,为了尽可能的减少 "过桥费",有以下几点需要注意:
来源: