DOM编程
用JavaScript进行DOM操作的代价很昂贵,它是富Web应用中最常见的性能瓶颈.
文档对象模型DOM是一个独立于语言,用于操作XML和HTML文档的程序接口(API).在浏览器中主要用来和HTML文档打交道.
尽管DOM是个与语言无关的API,它在浏览器中的接口却是用JavaScript实现的.客户端脚本编程大多时候是在底层文档打交道,DOM就成为现在JavaScript编码中的重要部分
浏览器中通常会把DOM和JavaScript单独实现,JavaScript引擎和DOM渲染引擎相互对立.
这对性能来说意味着什么?简单理解,两个相互对立的功能只要通过接口就能彼此连接,就会产生消耗.把DOM和JavaScript各比作一座岛屿,它们之间用收费桥梁连接.ECMAscript每次访问DOM,都会这过座桥,并交纳过桥费,访问的DOM次数越多,费用就越高.因此推荐的做法就是尽可能的减少过桥的次数
DOM的访问与修改
访问DOM元素是有代价的, 修改元素更为昂贵,因为它会导致浏览器重新计算页面的几何变化.
为了让你对DOM编程带来的性能问题有个量化的了解,下面是简单实例:
function renderHtml(){
for(let count = 0, ; count < 1000;count++ ){
document.getElementById("box").innerHTML += a;
}
}
这个函数循环修改页面元素内容.这段代码的问题在于,每次循环迭代,该元素都被访问两次,一次读取innerHTML属性,另一次是重写它.
换一种效率更高的方法,用局部变量储存修改的内容,在循环结束后一次性写入;
function renderHtml2(){
var content = '';
for(let count = 0; count < 1000; count++){
content += 'a';
}
document.getElementById("box").innerHTML += content;
}
如果未来要多次修改和访问DOM元素, 更好的方法将访问的元素用一个局部变量或者全局变量缓存起来, 减少访问DOM次数
const box = document.getElementById("box");//定义一个变量用来存储DOM元素, 方便以后多次访问
function renderHtml2(){
var content = '';
for(let count = 0; count < 1000; count++){
content += 'a';
}
box.innerHTML += content;
}
在所有浏览器中,修改后的版本都运行得更快. 访问DOM的次数越多,代码运行的速度就越慢, 因此通用的经验法则就是:减少访问DOM的次数,
把运算尽量留在ECMAscript这一端处理.
innerHTML和DOM方法的对比
多年来,在web开发社区围绕这个问题有着许多讨论,修改页面区域的最佳方案是用非标准但支持良好的innerHTML
属性呢?还是只用document.createElement
的原生DOM方法? 如果不考虑web标准, 答案是相差无几. 但是,除开最新版WebKit内核(Chrome和Safari)
之外的所有浏览器中,innerHTML会更快一些.
用innerHTML生产一个ul列表
const body = document.body;
let data = ['name','age','sex','prototype','hobby','height']
function renderUl(rednerData){//参数rednerData为生成的li文本内容数据
let content =['<ul>',, '</ul>'],
length = rednerData.length,
liList = '';
for(let i = 0; i < length; i++){
liList += '<li>' +rednerData[i]+ '</li>'
};
content[1] = liList;
let ul = content.reduce( (acc,cur) => acc + cur )
body.innerHTML = ul
}
使用DOM方法生成相同的ul列表
const body = document.body;
let data = ['name','age','sex','prototype','hobby','height']
function renderUl2(rednerData){//参数rednerData为生成的li文本内容数据
let ui = document.createElement("ul");
rednerData.forEach( item =>{
let li = document.createElement("li");
li.innerText = item;
ui.append(li)
})
body.append(ui)
}
在旧版本的浏览器中,innerHTMl的优势更加明显,但在新版本浏览器中优势不那么明显. 在基于WebKit内核的新版浏览器中恰恰相反; 使用DOM方法略胜一筹,因此, 最终选择哪种方法取决于你的用户经常使用的浏览器,以及你的编码习惯;
如果对于性能没有那么苛刻的要求,一般推荐使用原生的DOM方法,更容易养成好的编码习惯,所以你更应该根据可读性,稳定性,团队习惯,代码风格来综合决定使用哪种方式
HTML集合
HTML集合是包含DOM节点引用的类数组对象.以下方法的返回值就是一个集合.
- document.getElementByname();
- document.getElementsByClasssName();
- document.getElementsByTagName();
下面的属性同样返回HTML集合
documen.images
- 页面中所有的img元素
document.links
- 页面中所有的a元素
document.froms
- 页面中所有的表单元素
document.form[0].elements
- 页面中第一个表单的所有字段
以上方法和属性的返回值为HTML集合对象,这是个类似数组的列表.他们并不是真正的数组(因为没有push()和slice方法()之类的方法),但它提供了类似数组的length属性,并且可以通过数字索引访问列表中的元素,例如, document.images[0]返回集合中第一个元素.正如DOM标准中所定义的,HTML集以一种 假定实时态
,这意味着当底层文档对象更新时,它也会知道更新
事实上,HTML集合一直与文档保持连接,每次你需要最新的信息时,都会重复执行查询的过程.哪怕只是获取集合中的元素个数(即访问集合的length属性), 也是如此,这正是昂贵性能消耗的源头.
昂贵的集合
为了演示集合的实时性,考虑以下的代码片段
//一个意外的死循坏
let allDivs = document.getElementsByTagName("div");
for(let i = 0; i < allDivs.length; i++){
let div = document.createElement("div");
document.body.appendChild(div);
}
这段代码看上去只是简单把页面的div元素数量翻倍.它遍历现有的div元素,每次创建一个新的div并添加到body中.但事实上这是一个死循环,意外循环的退出条件allDivs.length每次迭代时都会增加,它反映的是底层文档的实时当前状态.
像这样遍历HTML集合可能会导致逻辑错误,而且也很慢,因为每次迭代都会执行查询操作.
通常来说在循环的条件控制语句中读取数组的length属性是不推荐的做法.而且读取一个集合的length比读取普通数组的length属性要慢很多,因为每次都要重新查询.
上面的代码可以改下成下面的代码, 把集合的length属性用一个局部变量保存起来,避免重复的读取集合的length属性,导致死循坏
在每次迭代过程中.读取元素的集合的length属性会引发集合进行更新,这是所有浏览器中都有的明显的性能问题,优化方法很简单吗把集合的长度缓存到一个局部变量,然后在循环退出的条件语句中使用该变量
let allDivs = document.getElementsByTagName("div"),
length = allDivs.length;
for(let i = 0; i <length; i++){
let div = document.createElement("div");
document.body.appendChild(div);
}
在相同的内容和数量上,遍历应该数组的速度明显快于遍历一个HTML集合.
考虑一个API把一个HTML集合拷贝成一个普通的数组.
let allDivs = document.getElementsByTagName("div"),
allDivsToArr = Array.form(allDivs);// es6新api把类数组转为真正的数组
for(let i = 0,length = allDivs.length;; i <length; i++){
let div = document.createElement("div");
document.body.appendChild(div);
}
很多情况下如果只需要遍历一个相对较小的集合,那么缓存length就够了.但是由于变遍历数组比遍历集合快,因此如果将集合元素拷贝到数字中,那么访问它的属性就更快.请记住,这会因额为的步骤带来消耗,而且会多遍历一遍集合,因此应当在评估特定的条件下评估使用数组拷贝是否有帮助.
访问集合元素中使用局部变量
一般来说,对于任何类型的DOM访问,需要多次访问同一个DOM属性或方法或方法需要多次访问,最好使用一个局部变量缓存此成员.当遍历一个集合时,第一优先原则就是把集合缓存到局部变量中, 并把length属性缓存在循坏外部,使用局部变量替代这些需要多次读取的元素
看下面的例子,在循坏体中读取元素的三个属性.最慢的版本每次都要读取全局的document,优化的版本缓存了一个集合的引用, 最快的版本把当前集合元素缓存到一个变量,这三个版本都缓存了集合的length属性.
//较慢
function collectionColbal(){
let coll = document.getElementsByTagName('div'),
len = coll.length,
name = '';
for(let 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 collectionColbal(){
let coll = document.getElementsByTagName('div'),
len = coll.length,
name = '';
for(let count = 0; count < len; count++){
name = coll[count].nodeName;
name = coll[count].nodeType;
name = coll[count].tagName;
}
return name;
}
//最快
function collectionColbal(){
let coll = document.getElementsByTagName('div'),
len = coll.length,
name = '',
el = null;
for(let count = 0; count < len; count++){
el = coll[count]//把集合元素缓存到一个变量
name = el.nodeName;
name = el.nodeType;
name = el.tagName;
}
return name;
}
在遍历DOM读取属性时,使用局部变量存储集合引用和集合元素可以带来显著的性能提升.
网友评论