1.什么是DOM
众所周知,js由三部分组成:DOM,ECMA,BOM。其中的DOM,就是用于操作XML和HTML文档的应用程序。我们在页面上能看到的所有标签,全部都是DOM节点 ,比如html标签下面有head,body标签,而head标签里面又可以有title、meta标签,body里面可以有div、p等等标签,这种很像一棵大树一样,树干分为枝干,枝干里面又有大大小小的树枝,所以也叫做DOM树。
2.JavaScript操作DOM
在浏览器内部,会把dom与js独立实现,可以理解成两个独立的小岛,所以用js操作dom的时候,就相当于我们从一个岛到另一个岛,每次都要划船过去,不同内核的浏览器,就相当于不同的海洋,不同的船夫,因此划船的时间,划船的距离,也是不一样的,我们对于dom性能优化的两个核心点,一个是减少划船的次数,能在岛内完成的就在岛内完成,一个是根据不同的大海选择合适的船夫。
现在在页面创建1000个无序列表,文本内容为0-9999,下面是两种不同的方法,为了方便进行性能对比,我们采用console.time来记录执行时间。
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
let str = "";
// 第一种方式
console.time("innerHTML遍历1000次消耗的时间:");
for (let i = 0; i < 1000; i++) {
oUl.innerHTML += `<li>${i}<li>`;
}
console.timeEnd("innerHTML遍历1000次消耗的时间:");
// 第二种方式
console.time("字符累加1000次消耗的时间:");
for (let i = 0; i < 1000; i++) {
str += `<li>${i}<li>`;
}
oUl.innerHTML = str;
console.timeEnd("字符累加1000次消耗的时间:");
</script>
</body>
下面是控制台打印出来的时间
上面遍历了1000次的innerHTML,就相当于从js岛到dom岛,划了1000次的船,而str字符串累加,只是js岛内操作,因此消耗的性能并不大。
3.innerHTML与DOM方法对比
为了方便测试性能消耗,我们把节点增加至10000,再分别用innerHTML和document.createElement, document.appendChild进行创建,观察相同的代码在谷歌和火狐这两个不同内核的下所花费的执行时间。
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
let str = "";
// innerHTML创建节点
console.time("innerHTML方法消耗的时间:");
for (let i = 0; i < 10000; i++) {
str += `<li>${i}<li>`;
}
oUl.innerHTML = str;
console.timeEnd("innerHTML方法消耗的时间:");
// appendChild创建节点
console.time("appendChild消耗的时间:");
for (let i = 0; i < 10000; i++) {
const oLi = document.createElement("li");
oUl.appendChild(oLi);
}
console.timeEnd("appendChild消耗的时间:");
</script>
</body>
谷歌浏览器下的控制台截图
火狐浏览器下的控制台截图
可以看到,对于谷歌内核的浏览器来说,appendChild的性能更好,而对于火狐内核的浏览器来说,innerHTML的性能更好。
4.减少DOM操作
- 节点克隆
下面我们来对上面的10000个节点进行优化,采用cloneNode替换原有的createElement,具体代码如下:
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
// appendChild创建节点
console.time("appendChild消耗的时间:");
for (let i = 0; i < 10000; i++) {
const oLi = document.createElement("li");
oUl.appendChild(oLi);
}
console.timeEnd("appendChild消耗的时间:");
// cloneNode克隆节点
console.time("cloneNode消耗的时间:");
const cloneLi = document.createElement("li");
for (let i = 0; i < 10000; i++) {
const oLi = cloneLi.cloneNode(true);
oUl.appendChild(oLi);
}
console.timeEnd("cloneNode消耗的时间:");
</script>
</body>
在谷歌浏览器中打印的日志如下
- 访问元素集合
下面我们对创建出来的10000个节点进行赋值,文本内容为0-9999,具体代码如下:
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
for (let i = 0; i < 10000; i++) {
const oLi = document.createElement("li");
oUl.appendChild(oLi);
}
const aLi = document.getElementsByTagName("li");
console.time("使用aLi.length获取节点消耗的时间:");
for (let i = 0; i < aLi.length; i++) {
aLi[i].innerText = i;
}
console.timeEnd("使用aLi.length获取节点消耗的时间:");
</script>
</body>
控制台打印如下
下面我们对上述代码进行一些改动,把节点数组的长度用一个布局变量存储起来
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
for (let i = 0; i < 10000; i++) {
const oLi = document.createElement("li");
oUl.appendChild(oLi);
}
const aLi = document.getElementsByTagName("li");
const len = aLi.length;
console.time("使用len获取节点消耗的时间:");
for (let i = 0; i < len; i++) {
aLi[i].innerText = i;
}
console.timeEnd("使用len获取节点消耗的时间:");
</script>
</body>
控制台打印如下
所以可以用局部变量去存储DOm节点的时候,尽量使用布局变量
const oDiv = document.getElementById()
const oInput = document.getElementById()
const oUl = document.getElementById()
// 上述操作,可以改为
const doc = document;
const oDiv = doc.getElementById()
const oInput = doc.getElementById()
const oUl = doc.getElementById()
- 元素节点
在获取一些元素节点时,尽量用只获取元素节点的方法,比如使用children而不使用childNodes,因为childNodes里面包含了元素节点和文本节点,而children只包含了元素节点,类似区别的还有firstChild和firstElementChild
- 选择器API
当节点嵌套很深的时候,尽量使用querySelector和querySelectorAll,而不是使用getElementById,getElementsByTagName,getElementsByClassName去一层层获取
5.DOM与浏览器
- 重排与重绘
重排是指改变页面内容,重绘是指改变浏览器显示内容。比如说,div的宽高,位置大小改变了,会触发重排,但是div的背景颜色改变,字体颜色改变,不会触发重排,只会触发重绘。
- 添加顺序
上面说了重排和重绘会触发浏览器的渲染,所以有时候一些看似不重要的先后顺序,也会对浏览器的性能消耗产生影响。
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
console.time("先appendChild后innerText消耗的时间:");
for (var i = 0; i < 10000; i++) {
var oLi = document.createElement("li");
oLi.innerHTML = "<li></li>";
oUl.appendChild(oLi);
oLi.innerText = i;
}
console.timeEnd("先appendChild后innerText消耗的时间:");
console.time("先innerText后appendChild消耗的时间:");
for (var i = 0; i < 10000; i++) {
var oLi = document.createElement("li");
oLi.innerHTML = "<li></li>";
oLi.innerText = i;
oUl.appendChild(oLi);
}
console.timeEnd("先innerText后appendChild消耗的时间:");
</script>
</body>
控制台打印如下
原因也很简单,第一种写法,先把元素添加到页面,后在改变他的文本值,触发了重排,因此时间会多一些
- 合并dom操作
在对dom进行css样式添加时,尽量使用cssText来合并相关的样式操作
<body>
<ul id="ul"></ul>
<script>
const oUl = document.getElementById("ul");
console.time("style设置样式消耗的时间:");
for (var i = 0; i < 10000; i++) {
var oLi = document.createElement("li");
oLi.innerHTML = "<li></li>";
oLi.innerText = i;
oLi.style.width = "100px";
oLi.style.height = "100px";
oLi.style.background = "red";
oUl.appendChild(oLi);
}
console.timeEnd("style设置样式消耗的时间:");
console.time("cssText设置样式消耗的时间:");
for (var i = 0; i < 10000; i++) {
var oLi = document.createElement("li");
oLi.innerHTML = "<li></li>";
oLi.innerText = i;
oLi.style.cssText = "width:100px;height:100px;background:red";
oUl.appendChild(oLi);
}
console.timeEnd("cssText设置样式消耗的时间:");
</script>
</body>
控制台打印如下