第14章 DOM
DOM:文档对象模型,是 html 的编程接口。从上到下依次是
- document
- html
- head
- body
- html
# Node 类型
每个节点都有 nodeType 属性,表示该节点的类型,共有 12 种类型,是 12 个数值常量。
开发中最常见的是元素节点和文本节点。
# 节点关系
每个节点都有 childNodes 属性,对应 NodeList 实例,是一个类数组对象,可以使用下面的方式转为真数组。
let nodes = Array.prototype.slice.call(someNode.childNodes, 0)
let nodes = Array.from(someNode.childNodes)
2
3
每个节点都有 parentNode 属性,指向其 DOM 树的父元素。
previousSibling 指向节点的上一个同胞节点,nextSibling 指向节点的下一个同胞节点。
父节点有 2 个专门属性,firstChild 表示第一个子节点,lastChild 表示最后一个子节点。
利用上述的节点关系,可以访问到文档树中的任何节点。
# 操纵节点
关系指针只能读,下面介绍几种操纵节点的方法。
appendChild()。用于在 childNodes 列表末尾添加节点,返回新添加的节点。如果添加的节点在文档中已存在,则这个节点会从之前的位置转移到新位置。
let returnedNode = someNode.appendChild(newNode) console.log(returnedNode === newNode) // true console.log(someNode.lastChild === returnedNode) // true let returnedNode = someNode.appendChild(someNode.firstChild) console.log(returnedNode === someNode.firstChild) // false console.log(someNode.lastChild === returnedNode) // true
1
2
3
4
5
6
7如果想在特定位置插入节点,可以使用 insertBefore(),接收 2 个参数:要插入的节点和参照节点。返回被插入的节点。
let returnedNode = someNode.insertBefore(newNode, null) console.log(returnedNode === someNode.lastChild) // true let returnedNode = someNode.insertBefore(newNode, someNode.firstChild) console.log(returnedNode === newNode) // true console.log(returnedNode === someNode.firstChild) // true
1
2
3
4
5
6replaceChild()则会替换节点。接收 2 个参数:要插入的节点和要替换的节点。被替换的节点将从文档完全移除。
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild)
1removeChild()则会移除该节点。
let returnedNode = someNode.removeChild(someNode.firstChild)
1
这 4 个方法用于操作某个节点的子元素,也就是说,必须先取得父节点。
此外,还有 2 个 DOM 方法,cloneNode()和 normalize(),就不介绍了。
# Document 类型
html 是根元素,获取方式有如下几种
- document.documentElement
- document.firstChild
- document.childNodes[0]
可以通过document.body
访问 body 元素。
document.title 可读可写,通常显示在页面标题栏。
document.URL 显示的页面完整的 URL,基本和 location.href 一样,只读。
document.domain 可读可写域名,注意不能设置 URL 中不包含的值,即只能放松,不能收紧。
// 页面是test.tencent.com
document.domain = 'tencent.com' // ok
document.domain = 'test.tencent.com' // 设置回去,出错
2
3
document.getElementById():最经典的获取 DOM 方式,根据 id 获取。
document.getElementByTagName():获取标签元素。
document.querySelector():selectors API 经典获取 DOM 方法,推荐。
document.querySelectorAll():查询所有匹配的节点,返回的是 NodeList 列表的静态快照。
# Element 类型
该节点有如下特点:
- nodeType 等于 1
- nodeName 值为元素的标签名
- nodeValue 为 null
HTML 元素拥有的属性有:
- id,元素在文档中的唯一标识符
- title,鼠标悬停时的提示信息
- className,指元素的 CSS 类
属性相关的 DOM 方法有 3 类:
- getAttribute():获取属性。自定义属性名一般写
data-
前缀。特殊类型有 2 类,1 是 style 属性,2 是事件处理程序。所以一般用该方法获取自定义属性。 - setAttribute():设置属性。有则替换,无则创建。
- removeAttribute():删除属性。是从元素中抹掉该属性,而不只是清除属性值。
创建元素:
document.createElement():之后可以给创建的属性添加属性和子元素,也可以将其挂载到文档树上。
let div = document.createElement('div') div.id = 123 div.className = 'hello' div.innerHTML = '我是一个div' document.body.appendChild(div)
1
2
3
4
5
6得到如下 html
<body> <div id="123" class="hello">我是一个div</div> </body>
1
2
3
# Text 类型
该节点有如下特征:
- nodeType 等于 3
- nodeName 为'#text'
- nodeValue 为节点中包含的文本
- 不支持子节点
除了 nodeValue,也可以使用 data 属性访问和设置 Text 节点中的文本。
假设有一个 div
<div>我是一个div</div>
let textNode = div.firstChild
console.log(textNode.nodeValue === textNode.data) // true
textNode.nodeValue = 'hello' // 文本改为 hello
textNode.data = 'world' // 文本改为 world
2
3
4
创建文本节点:
- document.createTextNode()
一般一个元素只包含一个文本子节点,实际可以有多个。
- normalize():将所有同胞文本节点合并为一个子节点。
- splitText():与 normalize()相反,指定在偏移位置拆分 nodeValue,形成 2 个文本节点。
# DocumentFragment 类型
该节点具有如下特征:
- nodeType 等于 11
- nodeName 为'#document-fragment'
- 不能直接把文档片段添加到文档,它不存在真实的文档树上。
- 如果文档中的一个节点被添加到文档片段,则该节点会从文档树中移除。
- 如果把文档片段添加到文档,则该文档片段的所有子节点都会转移到文档树,文档片段本身不会。
可以看出,文档片段的作用是充当其他要被添加到文档的节点的仓库,一般在优化 DOM 性能时,为避免多次渲染使用到。
比较下面 2 段代码。
// 代码1
const ul = document.querySelector('ul')
for (let i = 0; i < 3; i++) {
const li = document.createElement('li')
li.appendChild(document.createTextNode(`Item ${i + 1}`))
ul.appendChild(li)
}
2
3
4
5
6
7
8
// 代码2
const ul = document.querySelector('ul')
const fragment = document.createDocumentFragment()
for (let i = 0; i < 3; i++) {
const li = document.createElement('li')
li.appendChild(document.createTextNode(`Item ${i + 1}`))
fragment.appendChild(li)
}
ul.appendChild(fragment)
2
3
4
5
6
7
8
9
10
代码 1 和代码 2 都会呈现相同的结果,不同点在于代码 1 会渲染 3 次(每次添加列表项时,重新渲染新添加的内容),而代码 2 只渲染 1 次(使用文档片段添加所有列表项,并 没有 DOM 消耗,然后一次性添加到 ul 元素),减少了 DOM 操作。
# DOM 编程
# 动态脚本
有 2 种方式为网页添加脚本:
引入外部文件
<script src="foo.js"></script> // 或者通过DOM方法 <script> let script = document.createElement('script') script.src = 'foo.js' document.body.appendChild(script) </script>
1
2
3
4
5
6
7
8嵌入源代码
<script> console.log('hello') </script>
1
2
3
注意,通过 innerHTML 属性创建的<script>元素永远不会执行。比较下面两段代码。
const div1 = document.createElement('div')
let script = document.createElement('script')
script.innerHTML = `console.log('hello')`
div1.appendChild(script)
document.body.appendChild(div1)
// 输出hello
2
3
4
5
6
7
8
const div1 = document.createElement('div')
div1.innerHTML = `
<script>console.log('hello')<\/script>
`
document.body.appendChild(div1)
// 无输出
2
3
4
5
6
7
8
# 动态样式
CSS 样式可以通过两种方式加载。
<link>元素
<link rel="stylesheet" href="style.css" />
1<style>元素
<style> div { width: 100px; height: 100px; } </style>
1
2
3
4
5
6
# MutationObserver 接口
MutationObserver 可以在 DOM 被修改时异步执行回调。可以用于观察整个文档、DOM 树的一部分,或者某个元素。同时还可以观察属性、子节点、文本等变化。
# 基本用法
observe()
调用该方法来进行观察,传入 2 个参数:要观察的 DOM 节点,MutationObserverInit 对象。
MutationObserverInit 对象
属性 说明 subtree 是否观察目标节点的子树,默认 false attributes 是否观察目标节点的属性变化,默认 false attributeFilter 可以过滤的属性数组,默认观察所有属性 attributeOldValue MutationRecord 是否记录变化之前的属性值,默认 false characterData 是否观察文本节点中字符的变化,默认 false characterDataOldValue MutationRecord 是否记录变化之前的字符数据,默认 false childList 修改目标子节点是否会触发变化事件,默认 false 回调与 MutationRecord
节点的变化会触发回调,变化的信息保存在 MutationRecord 实例中,然后被添加到记录队列。记录队列是 DOM 变化事件的有序列表,回调结束后清空队列。
某个属性或节点进行连续修改的变化,会生成多个 MutationRecord 实例。
disconnect()与重新使用
可以调用 disconnect()提前终止执行回调,但它不会结束 MutationObserver 的生命,如果再次调用 observe(),将会重新使用这个观察者。
<style> div { width: 100px; height: 100px; } </style> <body> <button>点我</button> <div></div> </body>
1
2
3
4
5
6
7
8
9
10
11const button = document.querySelector('button') const div = document.querySelector('div') button.onclick = function() { let { background } = div.style background = background === 'pink' ? 'skyblue' : 'pink' div.style.background = background // 连续修改会生成多个MutationRecord div.id = '1' div.id = '2' div.setAttribute('foo', 'bar') div.setAttribute('foo', 'baz') div.removeAttribute('foo') } let observer = new MutationObserver((record) => { // console.log(record) for (let item of record) { console.log(`${item.attributeName} was changed`) } }) observer.observe(div, { attributes: true }) // 抛弃执行回调 observer.disconnect() // 重新使用观察者 observer.observe(div, { attributes: true })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 引用关系
MutationObserver 实例与目标节点之间的引用关系是非对称的。
MutationObserver 拥有对要观察的目标节点的弱引用,而目标节点拥有对 MutationObserver 的强引用,所以会影响到垃圾回收 MutationObserver。除非目标节点从 DOM 中移除 ,MutationObserver 才会被回收。