第14章 DOM

1 节点层级

1.1 Node 类型

每个节点都有 nodeType 属性,表示该节点的类型。节点类型由定义在 Node 类型上的 12 个数值
常量表示:
 Node.ELEMENT_NODE(1)
 Node.ATTRIBUTE_NODE(2)
 Node.TEXT_NODE(3)
 Node.CDATA_SECTION_NODE(4)
 Node.ENTITY_REFERENCE_NODE(5)
 Node.ENTITY_NODE(6)
 Node.PROCESSING_INSTRUCTION_NODE(7)
 Node.COMMENT_NODE(8)
 Node.DOCUMENT_NODE(9)
 Node.DOCUMENT_TYPE_NODE(10)
 Node.DOCUMENT_FRAGMENT_NODE(11)
 Node.NOTATION_NODE(12)
浏览器并不支持所有节点类型。开发者最常用到的是元素节点和文本节点。

1. nodeName 与 nodeValue

nodeName 与 nodeValue 保存着有关节点的信息。这两个属性的值完全取决于节点类型。

2. 节点关系

每个节点都有一个 childNodes 属性,其中包含一个 NodeList 的实例。NodeList 是一个类数组对象,用于存储可以按位置存取的有序节点。它其实是一个对 DOM 结构的查询,因此 DOM 结构的变化会自动地在 NodeList 中反映出来。我们通常说 NodeList 是实时的活动对象,而不是第一次访问时所获得内容的快照。

3. 操纵节点

appendChild()用于在 childNodes 列表末尾添加节点。

let returnedNode = someNode.appendChild(newNode); 
alert(returnedNode == newNode); // true 
alert(someNode.lastChild == newNode); // true

如果把文档中已经存在的节点传给 appendChild(),则这个节点会从之前的位置被转移到新位置。
即使 DOM 树通过各种关系指针维系,一个节点也不会在文档中同时出现在两个或更多个地方。因此,如果调用 appendChild()传入父元素的第一个子节点,则这个节点会成为父元素的最后一个子节点。

insertBefore()方法:

// 插入最后一个子节点前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild); 
alert(newNode == someNode.childNodes[someNode.childNodes.length - 2]); // true

replaceChild()方法接收两个参数:要插入的节点和要替换的节点。要替换的节点会被返回并从文档
树中完全移除,要插入的节点会取而代之:

// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
// 删除第一个子节点
let formerFirstChild = someNode.removeChild(someNode.firstChild);

4. 其他方法

cloneNode(),会返回与调用它的节点一模一样的节点。cloneNode()方法接收一个布尔值参数,表示是否深复制。在传入 true 参数时,会进行深复制,即复制节点及其整个子 DOM 树。如果传入 false,则只会复制调用该方法的节点,称为孤儿节点(orphan),这个方法只复制 HTML 属性,以及可选地复制子节点。除此之外则一概不会复制。IE 在很长时间内会复制事件处理程序,这是一个 bug,所以推荐在复制前先删除事件处理程序。

// myList保存着对这个<ul>元素的引用
let deepList = myList.cloneNode(true); 
alert(deepList.childNodes.length); // 3(IE9 之前的版本)或 7(其他浏览器)

1.2 Document 类型

1. 文档子节点

使用 documentElement 属性可以更快更直接地访问该元素。

let html = document.documentElement; // 取得对<html>的引用

2. 文档信息

// 取得完整的 URL 
let url = document.URL; 
// 取得域名
let domain = document.domain; 
// 取得来源
let referrer = document.referrer;

在每个页面上把 document.domain 设置为相同的值,这些页面就可以访问对方的 JavaScript
对象了。比如,一个加载自 www.wrox.com 的页面中包含一个内嵌窗格,其中的页面加载自
p2p.wrox.com。这两个页面的 document.domain 包含不同的字符串,内部和外部页面相互之间不能
访问对方的 JavaScript 对象。如果每个页面都把 document.domain 设置为 wrox.com,那这两个页面
之间就可以通信了。
把document.domain 设置为"wrox.com"之后,就不能再将其设置回"p2p.wrox.com",后者会导致错
误。

3. 定位元素

getElementById()、 getElementsByTagName()、getElementsByName

4. 特殊集合

document 对象上还暴露了几个特殊集合,这些集合也都是 HTMLCollection 的实例。这些集合是
访问文档中公共部分的快捷方式,列举如下。

  1. document.anchors 包含文档中所有带 name 属性的元素。

  2. document.applets 包含文档中所有元素(因为元素已经不建议使用,所 以这个集合已经废弃)。

  3. document.forms 包含文档中所有元素(与 document.getElementsByTagName
    (“form”) 返回的结果相同)。

  4. document.images 包含文档中所有元素(与 document.getElementsByTagName
    (“img”) 返回的结果相同)。

  5. document.links 包含文档中所有带 href 属性的元素。

5. DOM 兼容性检测

hasFeature()的返回值并不可靠。目前这个方法已经被废弃,不再建议使用。为了向后兼容,目前主流浏览器仍然支持这个方法,但无论检测什么都一律返回 true。

6. 文档写入

严格的 XHTML 文档不支持文档写入。

1.3 Element 类型

1. HTML 元素

HTML 元素上都有的标准属性:
 id,元素在文档中的唯一标识符;
 title,包含元素的额外信息,通常以提示条形式展示;
 lang,元素内容的语言代码(很少用);
 dir,语言的书写方向("ltr"表示从左到右,"rtl"表示从右到左,同样很少用);
 className,相当于 class 属性,用于指定元素的 CSS 类(因为 class 是 ECMAScript 关键字,
所以不能直接用这个名字)。

2. 取得属性

getAttribute()、setAttribute()和 removeAttribute()。这些方法主要用于操纵属性。

3. 设置属性

div.setAttribute("id", "someOtherId");

4. attributes 属性

Element 类型是唯一使用 attributes 属性的 DOM 节点类型。attributes 属性包含一个NamedNodeMap 实例,是一个类似 NodeList 的“实时”集合。元素的每个属性都表示为一个 Attr 节点,并保存在这个 NamedNodeMap 对象中。NamedNodeMap 对象包含下列方法:
 getNamedItem(name),返回 nodeName 属性等于 name 的节点;
 removeNamedItem(name),删除 nodeName 属性等于 name 的节点;
 setNamedItem(node),向列表中添加 node 节点,以其 nodeName 为索引;
 item(pos),返回索引位置 pos 处的节点。

5. 创建元素

可以使用 document.createElement()方法创建新元素。这个方法接收一个参数,即要创建元素的标签名。在 HTML 文档中,标签名是不区分大小写的,而 XML 文档(包括 XHTML)是区分大小写的。

6. 元素后代

元素可以拥有任意多个子元素和后代元素,因为元素本身也可以是其他元素的子元素。childNodes
属性包含元素所有的子节点。

1.4 Text 类型

Text 节点由 Text 类型表示,包含按字面解释的纯文本,也可能包含转义后的 HTML 字符,但不
含 HTML 代码。

1. 创建文本节点

let textNode = document.createTextNode("<strong>Hello</strong> world!");

2. 规范化文本节点

DOM 文档中也经常会出现两个相邻文本节点。为此,有一个方法可以合并相邻的文本节点。这个方法
叫 normalize(),是在 Node 类型中定义的(因此所有类型的节点上都有这个方法)。在包含两个或多
个相邻文本节点的父节点上调用 normalize()时,所有同胞文本节点会被合并为一个文本节点,这个
文本节点的 nodeValue 就等于之前所有同胞节点 nodeValue 拼接在一起得到的字符串。

3. 拆分文本节点

Text 类型定义了一个与 normalize()相反的方法——splitText()。这个方法可以在指定的偏移
位置拆分 nodeValue,将一个文本节点拆分成两个文本节点。

let element = document.createElement("div"); 
element.className = "message"; 
let textNode = document.createTextNode("Hello world!"); 
element.appendChild(textNode); 
document.body.appendChild(element); 
let newNode = element.firstChild.splitText(5); 
alert(element.firstChild.nodeValue); // "Hello" 
alert(newNode.nodeValue); // " world!" 
alert(element.childNodes.length); // 2

1.5 Comment 类型

注释节点可以作为父节点的子节点来访问。比如下面的 HTML 代码:

<div id="myDiv"><!-- A comment --></div>

1.6 CDATASection 类型

CDATASection 类型表示 XML 中特有的 CDATA 区块。CDATASection 类型继承 Text 类型,因
此拥有包括 splitText()在内的所有字符串操作方法。

1.7 DocumentType 类型

DocumentType 类型的节点包含文档的文档类型(doctype)信息

1.8 DocumentFragment 类型

在所有节点类型中,DocumentFragment 类型是唯一一个在标记中没有对应表示的类型。DOM 将
文档片段定义为“轻量级”文档,能够包含和操作节点,却没有完整文档那样额外的消耗。如果文档中的一个节点被添加到一个文档片段,则该节点会从文档树中移除,不会再被浏览器渲染。添加到文档片段的新节点同样不属于文档树,不会被浏览器渲染。

1.9 Attr 类型

元素数据在 DOM 中通过 Attr 类型表示。Attr 类型构造函数和原型在所有浏览器中都可以直接访
问。技术上讲,属性是存在于元素 attributes 属性中的节点。属性节点尽管是节点,却不被认为是 DOM 文档树的一部分。Attr 节点很少直接被引用,通常开发者更喜欢使用 getAttribute()、removeAttribute()和 setAttribute()方法操作属性。

2 DOM 编程

2.1 动态脚本

有两种方式通过script动态为网页添加脚本:引入外部文件和直接插入源代码。

2.2 动态样式

CSS 样式在 HTML 页面中可以通过两个元素加载。link元素用于包含 CSS 外部文件,而style
元素用于添加嵌入样式。与动态脚本类似,动态样式也是页面初始加载时并不存在,而是在之后才添加到页面中的。

2.3 操作表格

// 创建表格
let table = document.createElement("table"); 
table.border = 1; 
table.width = "100%"; 
// 创建表体
let tbody = document.createElement("tbody"); 
table.appendChild(tbody); 
// 创建第一行
tbody.insertRow(0); 
tbody.rows[0].insertCell(0); 
tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1")); 
tbody.rows[0].insertCell(1); 
tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1")); 
// 创建第二行
tbody.insertRow(1); 
tbody.rows[1].insertCell(0); 
tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2")); 
tbody.rows[1].insertCell(1); 
tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2")); 
// 把表格添加到文档主体
document.body.appendChild(table);

2.4 使用 NodeList

NodeList 对象和相关的 NamedNodeMap、HTMLCollection这3 个集合类型都是“实时的”,意味着文档结构的变化会实时地在它们身上反映出来,因此它们的值始终代表最新的状态。
任何时候要迭代 NodeList,最好再初始化一个变量保存当时查询时的长度,然后用循环变量与这
个变量进行比较,如下所示:

let divs = document.getElementsByTagName("div"); 
for (let i = 0, len = divs.length; i < len; ++i) { 
 let div = document.createElement("div"); 
 document.body.appendChild(div); 
}

3 MutationObserver 接口

MutationObserver 接口,可以在 DOM 被修改时异步执行回调。使用 MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。

3.1 基本用法

MutationObserver 的实例要通过调用 MutationObserver 构造函数并传入一个回调函数来创建:

let observer = new MutationObserver(() => console.log('DOM was mutated!'));

1. observe()方法

新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observer 与 DOM 关联起来,需要使用 observe()方法。这个方法接收两个必需的参数:要观察其变化的 DOM 节点,以及
一个 MutationObserverInit 对象。
MutationObserverInit 对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。例如,下面的代码会创建一个观察者(observer)并配置它观察元素上的属性变化:

let observer = new MutationObserver(() => console.log('<body> attributes changed')); 
observer.observe(document.body, { attributes: true });
document.body.className = 'foo'; 
console.log('Changed body class'); 
// Changed body class 
// <body> attributes changed

2. 回调与 MutationRecord

每个回调都会收到一个 MutationRecord 实例的数组。MutationRecord 实例包含的信息包括发生了什么变化,以及 DOM 的哪一部分受到了影响。

let observer = new MutationObserver( 
 (mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.setAttribute('foo', 'bar'); 
// [ 
// { 
// addedNodes: NodeList [],
// attributeName: "foo", 
// attributeNamespace: null, 
// nextSibling: null, 
// oldValue: null, 
// previousSibling: null 
// removedNodes: NodeList [], 
// target: body 
// type: "attributes" 
// } 
// ]

3. disconnect()方法

默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事件,从而被执行。要提前终止执行回调,可以调用 disconnect()方法。不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调:

let observer = new MutationObserver(() => console.log('<body> attributes changed')); 
observer.observe(document.body, { attributes: true }); 
document.body.className = 'foo'; 
observer.disconnect(); 
document.body.className = 'bar'; 
//(没有日志输出)

要想让已经加入任务队列的回调执行,可以使用 setTimeout()让已经入列的回调执行完毕再调用
disconnect():

setTimeout(() => { 
 observer.disconnect(); 
 document.body.className = 'bar'; 
}, 0);

4. 复用 MutationObserver

多次调用 observe()方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。此
时,MutationRecord 的 target 属性可以标识发生变化事件的目标节点。

let observer = new MutationObserver( 
 (mutationRecords) => console.log(mutationRecords.map((x) => 
x.target))); 
// 向页面主体添加两个子节点
let childA = document.createElement('div'), 
 childB = document.createElement('span'); 
document.body.appendChild(childA); 
document.body.appendChild(childB); 
// 观察两个子节点
observer.observe(childA, { attributes: true }); 
observer.observe(childB, { attributes: true });

5. 重用 MutationObserver

调用 disconnect()并不会结束 MutationObserver 的生命。还可以重新使用这个观察者,再将它关联到新的目标节点。

3.2 MutationObserverInit 与观察范围

MutationObserverInit 对象用于控制对目标节点的观察范围。粗略地讲,观察者可以观察的事件包括属性变化、文本变化和子节点变化

1. 观察属性

MutationObserver 可以观察节点属性的添加、移除和修改。要为属性变化注册回调,需要在MutationObserverInit 对象中将 attributes 属性设置为 true,如下:

let observer = new MutationObserver(
 (mutationRecords) => console.log(mutationRecords)); 
observer.observe(document.body, { attributes: true });

2. 观察字符数据

MutationObserver 可以观察文本节点(如 Text、Comment 或 ProcessingInstruction 节点)中字符的添加、删除和修改。要为字符数据注册回调,需要在 MutationObserverInit 对象中将characterData 属性设置为 true,如下所示:

let observer = new MutationObserver( 
 (mutationRecords) => console.log(mutationRecords)); 
// 创建要观察的文本节点
document.body.firstChild.textContent = 'foo'; 
observer.observe(document.body.firstChild, { characterData: true });

3. 观察子节点

MutationObserver 可以观察目标节点子节点的添加和移除。要观察子节点,需要在 MutationObserverInit 对象中将 childList 属性设置为 true。

4. 观察子树

默认情况下,MutationObserver 将观察的范围限定为一个元素及其子节点的变化。可以把观察
的范围扩展到这个元素的子树(所有后代节点),这需要在 MutationObserverInit 对象中将 subtree
属性设置为 true。

3.3 异步回调与记录队列

MutationObserver 接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。为了在大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存在 MutationRecord实例中,然后添加到记录队列。这个队列对每个 MutationObserver 实例都是唯一的,是所有 DOM变化事件的有序列表。

1. 记录队列

每次 MutationRecord 被添加到 MutationObserver 的记录队列时,仅当之前没有已排期的微任务回调时(队列中微任务长度为 0),才会将观察者注册的回调(在初始化 MutationObserver 时传入)作为微任务调度到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。

不过在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一
个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一
个实例,因为函数退出之后这些实现就不存在了。回调执行后,这些 MutationRecord 就用不着了,
因此记录队列会被清空,其内容会被丢弃。

2. takeRecords()方法

调用 MutationObserver 实例的 takeRecords()方法可以清空记录队列,取出并返回其中的所有 MutationRecord 实例。看这个例子:

let observer = new MutationObserver( 
 (mutationRecords) => console.log(mutationRecords)); 
observer.observe(document.body, { attributes: true }); 
document.body.className = 'foo'; 
document.body.className = 'bar'; 
document.body.className = 'baz'; 
console.log(observer.takeRecords()); 
console.log(observer.takeRecords()); 
// [MutationRecord, MutationRecord, MutationRecord] 
// []

3.4 性能、内存与垃圾回收

DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。由于浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3 规定废弃了这些事件。MutationObserver 接口就是为替代这些事件而设计的更实用、性能更好的方案。

1. MutationObserver 的引用

目标节点却拥有对 MutationObserver 的强引用。如果目标节点从 DOM 中被移除,随后被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。

2. MutationRecord 的引用

有时候可能需要保存某个观察者的完整变化记录。保存这些 MutationRecord 实例,也就会保存
它们引用的节点,因而会妨碍这些节点被回收。如果需要尽快地释放内存,建议从每个MutationRecord中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃 MutationRecord。

4 小结

要理解 DOM,最关键的一点是知道影响其性能的问题所在。DOM 操作在 JavaScript 代码中是代价
比较高的,NodeList 对象尤其需要注意。NodeList 对象是“实时更新”的,这意味着每次访问它都
会执行一次新的查询。考虑到这些问题,实践中要尽量减少 DOM 操作的数量。

MutationObserver 是为代替性能不好的 MutationEvent 而问世的。使用它可以有效精准地监控
DOM 变化,而且 API 也相对简单。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值