DOM结构
1、DOM(Document Object Model,文档对象模型)
文档:html页面
文档对象:页面中的元素
文档对象模型:为了让程序(js)去操作页面中的元素而定义的标准。通过DOM,可访问HTML文档的所有元素
把整个文档看成一棵树,树分支就是节点,同时定义了很多方法来操作这些节点
2、DOM树中常见的DOM节点
(1)元素节点:<a>,<h1>,<body>,<html>
等,即标签
(2)文本节点:向用户展示的内容(元素节点和属性节点的内容),如
DOM结构优化
1、在优化之前,先了解DOM为什么这么慢
- 因为收了“过路费”
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》
JS引擎和渲染引擎是独立实现的,当我们用JS去操作DOM时,本质上是JS引擎和渲染引擎两个模块之间的协作。我们每操作一次DOM(无论是修改还是仅仅访问其值),都要过一次“桥”。过”桥“的次数一多,就会产生明显的性能问题。因此,要“减少DOM操作”。
- 对DOM的修改引发样式的更迭
过桥很慢,到了桥对岸,我们更改操作带来的结果也很慢。
我们对DOM的操作很多时候不仅是访问它,更多是修改它,当我们对DOM的修改会引起它样式上的改变时,就会触发回流和重绘。
2、DOM优化 - 减少DOM访问次数
- 多次访问同一DOM,应用局部变量缓存该DOM
- 尽可能使用querySelector,而不是获取HTML集合的API
- 注意重排重绘
- 使用事件委托【用addEventListener(type,listener,useCapture)实现】,减少绑定事件的数量
一个小栗子:
首先有如下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>DOM操作测试</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
此时有一个假需求——向container元素里写10000句一样的话,如果这样做:
for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
}
这段代码有两个明显的可优化点。
第一点:我们每一次循环都调用 DOM 接口重新获取了一次 container 元素,相当于每次循环都去操作DOM,这样不必要的DOM操作过多。我们可以通过缓存变量的方式来节省不必要的渲染:
// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){
container.innerHTML += '<span>我是一个小测试</span>'
}
第二点,不必要的 DOM 更改太多了。我们的 10000 次循环里,修改了 10000 次 DOM 树。我们前面说过,对 DOM 的修改会引发渲染树的改变、进而去走一个(可能的)回流或重绘的过程,而这个过程的开销是很“贵”的。这么贵的操作,我们竟然重复执行了 N 多次!其实我们可以通过就事论事的方式节省下来不必要的渲染:
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){
// 先对内容进行操作
content += '<span>我是一个小测试</span>'
}
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content
就事论事,即JS层面的事情,JS自己去处理,处理好再来找DOM。
在上面的例子里,字符串变量content扮演 一个DOM Fragment 的角色,他们本质上都作为脱离真实DOM树的容器出现,用于缓存批量化的DOM操作。
前面我们直接用innerHTML去拼接目标内容,但DOM Fragment可以帮我们更加结构化的方式达成同样的目的,从而在维持性能的同时,保住代码的可拓展和可维护性:
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一个小测试'
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
浏览器工作的过程:
- 从网络或本地拿到html源代码
- 浏览器将html源码中的每个标签都实例化对应的对象
- 在内存中形成一个DOM树
- 将内存DOM树解析为可视页面
- JS直接修改DOM树,改变页面视图