title: JavaScript高级程序设计第四版学习–第十四章
date: 2021-5-25 22:40:38
author: Xilong88
tags: JavaScript
**
本章内容:
理解文档对象模型(DOM)的构成
节点类型
浏览器兼容性
MutationObserver 接口
可能出现的面试题:
1.什么是DOM?
2.操作节点的程序题
3.节点和元素是什么?它们的的区别?
4.了解过MutationObserver吗?
5.写一个MutationObserver的程序之类的**
总结:本章讲了DOM和DOM的节点、元素,属性,对象和操作。比较重要,特别是对DOM的获取和操作的内容,下一章还会继续讲。
知识点:.
1.文档对象模型DOM
是HTML和XML文档的编程接口。DOM表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。
节点层级:
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
2.Node 类型
Node 接口在JavaScript中被实现为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)
节点类型可通过与这些常量比较来确定,如:
if (someNode.nodeType == Node.ELEMENT_NODE){
alert("Node is an element.");
}
nodeName 与nodeValue
nodeName 与nodeValue 保存着有关节点的信息。这两个属性的值完全取决于节点类型。在使用这两个属性前,最好先检测节点类型,如下所示:
if (someNode.nodeType == 1){
value = someNode.nodeName; // 会显示元素的标签名
}
在这个例子中,先检查了节点是不是元素。如果是,则将其nodeName 的值赋给一个变量。对元素而言,nodeName 始终等于元素的标签名,而nodeValue 则始终为null 。
childNodes 属性,包含NodeList,它不是数组的实例,是一个类数组对象.
转成数组:
let arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);
也可以这样ES6新方法:
let arrayOfNodes = Array.from(someNode.childNodes);
previousSibling 和nextSibling,返回兄弟节点
firstChild 和lastChild ,第一个自节点和最后一个子节点
hasChildNodes(),是否有子节点
ownerDocument 属性,指向文档节点
3.操作节点
appendChild(),添加到后面,返回添加的节点
insertBefore() ,添加到前面,返回添加的节点
replaceChild() 方法接收两个参数:要插入
的节点和要替换的节点。要替换的节点会被返回并从文档树中完全移除,要插入的节点会取而代之。
// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
// 替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);
removeChild(),removeChild() 方法。这个方法接收一个参数,即要移除的节点。被移除的节点会被返回
// 删除第一个子节点
let formerFirstChild = someNode.removeChild(someNode.firstChild);
// 删除最后一个子节点
let formerLastChild = someNode.removeChild(someNode.lastChild);
cloneNode(),接收一个布尔值参数,表示是否深复制
在传入true 参数时,会进行深复制,即复制节点及其整个子DOM树。如果传入false ,则只会复制调用该方法的节点。复制返回的节点属于文档所有,但尚未指定父节点,所以可称为孤儿节点(orphan)
normalize() ,如果发现空文本节点,则将其删除;如果两个同胞节点是相邻的,则将其合并为一个文本节点。
4.Document 类型
Document 类型是JavaScript中表示文档节点的类型。在浏览器中,文档对象document 是HTMLDocument 的实例(HTMLDocument 继承Document ),表示整个HTML页面。document 是window 对象的属性,因此是一个全局对象。
nodeType 等于9;
nodeName 值为"#document" ;
nodeValue 值为null ;
parentNode 值为null ;
ownerDocument 值为null ;
子节点可以是DocumentType (最多一个)、Element (最多一
个)、ProcessingInstruction 或Comment 类型。
Document 类型可以表示HTML页面或其他XML文档,但最常用的还是通过HTMLDocument 的实例取得document 对象。
document.documentElement直接获取html元素
document.body获取body元素
document.doctype获取<!doctype>元素
document是只读的,所以不能用方法增加删除html节点
document.title获取title元素
// 读取文档标题
let originalTitle = document.title;
// 修改文档标题
document.title = "New page title";
// 取得完整的URL
let url = document.URL;
// 取得域名
let domain = document.domain;
// 取得来源
let referrer = document.referrer;
在这些属性中,只有domain 属性是可以设置的。出于安全考虑,给domain 属性设置的值是有限制的。如果URL包含子域名如p2p.wrox.com ,则可以将domain 设置为"wrox.com" (URL包含“www”时也一样,比如www.wrox.com )。不能给这个属性设置URL中不包含的值,比如:
// 页面来自p2p.wrox.com
document.domain = "wrox.com"; // 成功
document.domain = "nczonline.net"; // 出错!
浏览器对domain 属性还有一个限制,即这个属性一旦放松就不能再收紧。比如,把document.domain 设置为"wrox.com" 之后,就不能再将其设置回"p2p.wrox.com" ,后者会导致错误,比如:
// 页面来自p2p.wrox.com
document.domain = "wrox.com"; // 放松,成功
document.domain = "p2p.wrox.com"; // 收紧,错误!
getElementById() (只返回第一个匹配的)
getElementsByTagName()
返回HTMLCollection对象。
HTMLCollection 对象还有一个额外的方法namedItem() ,可通过标签的name 属性取得某一项的引用
<img src="myimage.gif" name="myImage">
let myImage = images.namedItem("myImage");
这样,HTMLCollection 就提供了除索引之外的另一种获取列表项的方式,从而为取得元素提供了便利。对于name 属性的元素,还
可以直接使用中括号来获取,如下面的例子所示:
let myImage = images["myImage"];
数值索引会调用item() ,字符串索引会调用namedItem()
匹配全部
let allElements = document.getElementsByTagName("*");
document.getElementsByTagName() 方法,虽然规范要求区分标签的大小写,但为了最大限度兼容原有HTML页面,实际上是不区分大小写的。
getElementsByName()
通过name获取
document.anchors 包含文档中所有带name 属性的<a> 元
素。
document.applets 包含文档中所有<applet> 元素(因为
<applet> 元素已经不建议使用,所以这个集合已经废弃)。
document.forms 包含文档中所有<form> 元素
(与document.getElementsByTagName ("form") 返回的结
果相同)。
document.images 包含文档中所有<img> 元素
(与document.getElementsByTagName ("img") 返回的结
果相同)。
document.links 包含文档中所有带href 属性的<a> 元素。
DOM兼容性检测
document.implementation,
let hasXmlDom = document.implementation.hasFeature("XML", "1.0");
5.write() 、writeln() 、open() 和
close() 写入文本,写入文本自动换行,打开和关闭网页输出流。
6.Element 类型
nodeType 等于1;
nodeName 值为元素的标签名;
nodeValue 值为null ;
parentNode 值为Document 或Element 对象;
子节点可以是Element 、Text 、Comment
、ProcessingInstruction 、CDATASection
、EntityReference 类型。
可以通过nodeName 或tagName 属性来获取元素的标签名。这两个属性返回同样的值(添加后一个属性明显是为了不让人误会)。比如有下面的元素:
<div id="myDiv"></div>
可以像这样取得这个元素的标签名:
let div = document.getElementById("myDiv");
alert(div.tagName); // "DIV"
alert(div.tagName == div.nodeName); // true
元素标签名始终以全大写表示
属性:
id ,元素在文档中的唯一标识符;
title ,包含元素的额外信息,通常以提示条形式展示;
lang ,元素内容的语言代码(很少用);
dir ,语言的书写方向("ltr" 表示从左到右,"rtl" 表示从
右到左,同样很少用);
className ,相当于class 属性,用于指定元素的CSS类(因
为class 是ECMAScript关键字,所以不能直接用这个名
字)。
getAttribute()、setAttribute() 和removeAttribute()
let div = document.getElementById("myDiv");
alert(div.getAttribute("id")); // "myDiv"
alert(div.getAttribute("class")); // "bd"
alert(div.getAttribute("title")); // "Body text"
alert(div.getAttribute("lang")); // "en"
alert(div.getAttribute("dir")); // "ltr"
属性名不区分大小写,因此"ID" 和"id" 被认为是同一个属性。另外,根据HTML5规范的要求,自定义属性名应该前缀data-以方便验证。
使用getAttribute() 访问style属性时,返回的是
CSS字符串
通过DOM对象的属性访问时,style 属性返回的是一个(CSSStyleDeclaration )对象
getAttribute() 访问事件属性,
则返回的是字符串形式的源代码。
通过DOM对象的属性访问事件属性时返回的则是一个JavaScript函数
getAttribute() 主要用于取得自定义属性的值。
setAttribute() ,这个方法接
收两个参数:要设置的属性名和属性的值。如果属性已经存在,则setAttribute() 会以指定的值替换原来的值;如果属性不存在,则setAttribute() 会以指定的值创建该属性。.
div.setAttribute("id", "someOtherId");
div.setAttribute("class", "ft");
div.setAttribute("title", "Some other text");
div.setAttribute("lang","fr");
div.setAttribute("dir", "rtl");
使用setAttribute() 方法设置的属性名会规范为小写形式,因此"ID" 会变成"id" 。
在DOM对象上添加自定义属性,如下面的例子所示,不会自动让它变成元素的属性:
div.mycolor = “red”;
alert(div.getAttribute(“mycolor”)); // null(IE除外)
removeAttribute() 用于从元素中删除属性。这样不单单是清除属性的值,而是会把整个属性完全从元素中去掉,如下所示:
div.removeAttribute("class");
attributes 属性
attributes 属性包含一个NamedNodeMap 实例,是一个类似NodeList 的“实时”集合。元素的每个属性都表示为一个Attr 节点,并保存在这个NamedNodeMap 对象中
NamedNodeMap 对象包含下列方法:
getNamedItem(name ) ,返回nodeName 属性等于 name 的节点;
removeNamedItem(name ) ,删除nodeName 属性等于 name的节点;
setNamedItem(node ) ,向列表中添加 node 节点,以其nodeName 为索引;
item(pos ) ,返回索引位置 pos 处的节点。
let id = element.attributes.getNamedItem("id").nodeValue;
let id = element.attributes["id"].nodeValue;
element.attributes["id"].nodeValue = "someOtherId";
removeNamedItem() 方法与元素上removeAttribute() 方法类似,也是删除指定名字的属性。
attributes 属性最有用的场景是需要迭代元素上所有属性的时候
function outputAttributes(element) {
let pairs = [];
for (let i = 0, len = element.attributes.length; i < len; ++i) {
const attribute = element.attributes[i];
pairs.push(`${attribute.nodeName}="${attribute.nodeValue}"`);
}
return pairs.join(" ");
}
创建元素
document.createElement()
7.Text 类型
nodeType 等于3;
nodeName 值为"#text" ;
nodeValue 值为节点中包含的文本;
parentNode 值为Element 对象;
不支持子节点。
appendData(text ) ,向节点末尾添加文本 text ;
deleteData(offset, count ) ,从位置 offset 开始删除
count 个字符;
insertData(offset, text ) ,在位置 offset 插入 text ;
replaceData(offset, count, text ) ,用 text 替换从位置
offset 到 offset + count 的文本;
splitText(offset ) ,在位置 offset 将当前文本节点拆分为
两个文本节点;
substringData(offset, count ) ,提取从位置 offset 到
offset + count 的文本。
<!-- 没有内容,因此没有文本节点 -->
<div></div>
<!-- 有空格,因此有一个文本节点 -->
<div> </div>
<!-- 有内容,因此有一个文本节点 -->
<div>Hello World!</div>
document.createTextNode() 可以用来创建新文本节点
normalize()
let element = document.createElement("div");
element.className = "message";
let textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
let anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);
alert(element.childNodes.length); // 2
element.normalize();
alert(element.childNodes.length); // 1
alert(element.firstChild.nodeValue); // "Hello world!Yippee!"
splitText()
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
8.Comment 类型
DOM中的注释通过Comment 类型表示
nodeType 等于8;
nodeName 值为"#comment" ;
nodeValue 值为注释的内容;
parentNode 值为Document 或Element 对象;
不支持子节点。
document.createComment() 方法创建注释节点
注释的实际内容可以通过nodeValue 或data 属性获得
9.DocumentType
nodeType 等于10;
nodeName 值为文档类型的名称;
nodeValue 值为null ;
parentNode 值为Document 对象;
不支持子节点。
DOM Level 1规定了DocumentType 对
象的3个属性:name 、entities 和notations 。其中,name 是文档
类型的名称,entities 是这个文档类型描述的实体的NamedNodeMap
,而notations 是这个文档类型描述的表示法的NamedNodeMap 。
10.Attr 类型
元素数据在DOM中通过Attr 类型表示。Attr 类型构造函数和原型在所有浏览器中都可以直接访问。技术上讲,属性是存在于元素attributes 属性中的节点。
nodeType 等于2;
nodeName 值为属性名;
nodeValue 值为属性值;
parentNode 值为null ;
在HTML中不支持子节点;
在XML中子节点可以是Text 或EntityReference 。
Attr 对象上有3个属性:name 、value 和specified 。其中,name 包含属性名(与nodeName 一样),value 包含属性值(与nodeValue 一样),而specified 是一个布尔值,表示属性使用的是默认值还是被指
定的值。
let attr = document.createAttribute("align");
attr.value = "left";
element.setAttributeNode(attr);
alert(element.attributes["align"].value); // "left"
alert(element.getAttributeNode("align").value); // "left"
alert(element.getAttribute("align")); // "left"
11.DOM编程
<script src="foo.js"></script>
let script = document.createElement("script");
script.src = "foo.js";
document.body.appendChild(script);
在上面最后一行把
<script>
function sayHi() {
alert("hi");
}
</script>
let script = document.createElement("script");
script.appendChild(document.createTextNode("function sayHi(){alert('hi');}"));
document.body.appendChild(script);
<link rel="stylesheet" type="text/css" href="styles.css">
let link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "styles.css";
let head = document.getElementsByTagName("head")[0];
head.appendChild(link);
<style type="text/css">
body {
background-color: red;
}
</style>
let style = document.createElement("style");
style.type = "text/css";
style.appendChild(document.createTextNode("body{background-color:red}"));
let head = document.getElementsByTagName("head")[0];
head.appendChild(style);
操作表格
<table border="1" width="100%">
<tbody>
<tr>
<td>Cell 1,1</td>
<td>Cell 2,1</td>
</tr>
<tr>
<td>Cell 1,2</td>
<td>Cell 2,2</td>
</tr>
</tbody>
</table>
// 创建表格
let table = document.createElement("table");
table.border = 1;
table.width = "100%";
// 创建表体
let tbody = document.createElement("tbody");
table.appendChild(tbody);
// 创建第一行
let row1 = document.createElement("tr");
tbody.appendChild(row1);
let cell1_1 = document.createElement("td");
cell1_1.appendChild(document.createTextNode("Cell 1,1"));
row1.appendChild(cell1_1);
let cell2_1 = document.createElement("td");
cell2_1.appendChild(document.createTextNode("Cell 2,1"));
row1.appendChild(cell2_1);
// 创建第二行
let row2 = document.createElement("tr");
tbody.appendChild(row2);
let cell1_2 = document.createElement("td");
cell1_2.appendChild(document.createTextNode("Cell 1,2"));
row2.appendChild(cell1_2);
let cell2_2= document.createElement("td");
cell2_2.appendChild(document.createTextNode("Cell 2,2"));
row2.appendChild(cell2_2);
// 把表格添加到文档主体
document.body.appendChild(table);
<table>
元素添加了以下属性和方法:
caption ,指向<caption> 元素的指针(如果存在);
tBodies ,包含<tbody> 元素的HTMLCollection ;
tFoot ,指向<tfoot> 元素(如果存在);
tHead ,指向<thead> 元素(如果存在);
rows ,包含表示所有行的HTMLCollection ;
createTHead() ,创建<thead> 元素,放到表格中,返回引用;
createTFoot() ,创建<tfoot> 元素,放到表格中,返回引用;
createCaption() ,创建<caption> 元素,放到表格中,返回引
用;
deleteTHead() ,删除<thead> 元素;
deleteTFoot() ,删除<tfoot> 元素;
deleteCaption() ,删除<caption> 元素;
deleteRow(pos ) ,删除给定位置的行;
insertRow(pos ) ,在行集合中给定位置插入一行。
<tbody>
元素添加了以下属性和方法:
rows ,包含<tbody> 元素中所有行的HTMLCollection ;
deleteRow(pos ) ,删除给定位置的行;
insertRow(pos ) ,在行集合中给定位置插入一行,返回该行的引用。
<tr> 元素添加了以下属性和方法:
cells ,包含<tr> 元素所有表元的HTMLCollection ;
deleteCell(pos ) ,删除给定位置的表元;
insertCell(pos ) ,在表元集合给定位置插入一个表元,返回该
表元的引用。
12.关于NodeList
理解NodeList 对象和相关的NamedNodeMap 、HTMLCollection ,是理解DOM编程的关键。这3个集合类型都是“实时的”,意味着文档结构的变化会实时地在它们身上反映出来,因此它们的值始终代表最新的状态。
例如,下面的代码会导致无穷循环:
let divs = document.getElementsByTagName("div");
for (let i = 0; i < divs.length; ++i){
let div = document.createElement("div");
document.body.appendChild(div);
}
改为:
let divs = document.getElementsByTagName("div");
for (let i = 0, len = divs.length; i < len; ++i) {
let div = document.createElement("div");
document.body.appendChild(div);
}
或者:
let divs = document.getElementsByTagName("div");
for (let i = divs.length - 1; i >= 0; --i) {
let div = document.createElement("div");
document.body.appendChild(div);
}
13.MutationObserver 接口
可以在DOM被修改时异步执行回调
let observer = new MutationObserver(() => console.log('DOM was mutated!'));
observe() 方法
接收两个必需的参数:要观察其变化的DOM节点,以及一个MutationObserverInit 对象
MutationObserverInit 对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, { attributes: true });
后代不会触发
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
每个回调都会收到一个MutationRecord 实例的数组
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"
// }
// ]
连续修改会生成多个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';
// [MutationRecord, MutationRecord, MutationRecord]
下表列出了MutationRecord 实例的属性
let observer = new MutationObserver(
(mutationRecords, mutationObserver) => console.log(mutationRecords,
mutationObserver));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
// [MutationRecord], MutationObserver
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():
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
setTimeout(() => {
observer.disconnect();
document.body.className = 'bar';
}, 0);
// <body> attributes changed
复用MutationObserver
多次调用observe() 方法,可以复用一个MutationObserver 对象观察多个不同的目标节点。
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 });
// 修改两个子节点的属性
childA.setAttribute('foo', 'bar');
childB.setAttribute('foo', 'bar');
// [<div>, <span>]
disconnect() 方法是一个“一刀切”的方案,调用它会停止观察所有目标
重用MutationObserver
调用disconnect() 并不会结束MutationObserver 的生命。还可以重新使用这个观察者,再将它关联到新的目标节点。
let observer = new MutationObserver(() => console.log('<body> attributes
changed'));
observer.observe(document.body, { attributes: true });
// 这行代码会触发变化事件
document.body.setAttribute('foo', 'bar');
setTimeout(() => {
observer.disconnect();
// 这行代码不会触发变化事件
document.body.setAttribute('bar', 'baz');
}, 0);
setTimeout(() => {
// Reattach
observer.observe(document.body, { attributes: true });
// 这行代码会触发变化事件
document.body.setAttribute('baz', 'qux');
}, 0);
// <body> attributes changed
// <body> attributes changed
MutationObserverInit 与观察范围
在调用observe() 时,MutationObserverInit 对象中的attribute 、characterData 和childList 属性必须至少有一项为true (无论是直接设置这几个属性,还是通过设置attributeOldValue 等属性间接导致它们的值转换为true )。
否则会抛出错误,因为没有任何变化事件可能触发回调。
MutationObserver 可以观察节点属性的添加、移除和修改。要为属性变化注册回调,需要在MutationObserverInit 对象中将attributes 属性设置为true
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
// 添加属性
document.body.setAttribute('foo', 'bar');
// 修改属性
document.body.setAttribute('foo', 'baz');
// 移除属性
document.body.removeAttribute('foo');
// 以上变化都被记录下来了
// [MutationRecord, MutationRecord, MutationRecord]
把attributes 设置为true 的默认行为是观察所有属性,但不会在MutationRecord 对象中记录原来的属性值。如果想观察某个或某几个属性,可以使用attributeFilter 属性来设置白名单,即一个属性名字符串数组:
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributeFilter: ['foo'] });
// 添加白名单属性
document.body.setAttribute('foo', 'bar');
// 添加被排除的属性
document.body.setAttribute('baz', 'qux');
// 只有foo属性的变化被记录了
// [MutationRecord]
如果想在变化记录中保存属性原来的值,可以
将attributeOldValue 属性设置为true :
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue)));
observer.observe(document.body, { attributeOldValue: true });
document.body.setAttribute('foo', 'bar');
document.body.setAttribute('foo', 'b
document.body.setAttribute('foo', 'qux');
// 每次变化都保留了上一次的值
// [null, 'bar', 'baz']
观察字符数据
characterData 属性设置为true
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
// 创建要观察的文本节点
document.body.innerText = 'foo';
observer.observe(document.body.firstChild, { characterData: true });
// 赋值为相同的字符串
document.body.innerText = 'foo';
// 赋值为新字符串
document.body.innerText = 'bar';
// 通过节点设置函数赋值
document.body.firstChild.textContent = 'baz';
// 以上变化都被记录下来了
// [MutationRecord, MutationRecord, MutationRecord]
将characterData 属性设置为true 的默认行为不会
在MutationRecord 对象中记录原来的字符数据。如果想在变化记录中保存原来的字符数据,可以将characterDataOldValue 属性设置为true
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue)));
2
document.body.innerText = 'foo';
observer.observe(document.body.firstChild, { characterDataOldValue: true });
document.body.innerText = 'foo';
document.body.innerText = 'bar';
document.body.firstChild.textContent = 'baz';
// 每次变化都保留了上一次的值
// ["foo", "foo", "bar"]
观察子节点
需要在MutationObserverInit 对象中将childList
属性设置为true
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));
// [
// {
// addedNodes: NodeList[div],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: null,
// removedNodes: NodeList[],
// target: body,
// type: "childList",
// }
// ]
下面的例子演示了移除子节点:
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));
// [
// {
// addedNodes: NodeList[],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: null,
// removedNodes: NodeList[div],
// target: body,
// type: "childList",
// }
// ]
对子节点重新排序 (尽管调用一个方法即可实现)会报告两次变化事件,因为从技术上会涉及先移除和再添加:
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
// 创建两个初始子节点
document.body.appendChild(document.createElement('div'));
document.body.appendChild(document.createElement('span'));
observer.observe(document.body, { childList: true });
// 交换子节点顺序
document.body.insertBefore(document.body.lastChild, document.body.firstChild);
// 发生了两次变化:第一次是节点被移除,第二次是节点被添加
// [
// {
// addedNodes: NodeList[],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: div,
// removedNodes: NodeList[span],
// target: body,
// type: childList,
// },
// {
// addedNodes: NodeList[span],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: div,
// previousSibling: null,
// removedNodes: NodeList[],
// target: body,
// type: "childList",
// }
// ]
观察子树
需要在MutationObserverInit 对象中将subtree
属性设置为true
/ 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
// 创建一个后代
document.body.appendChild(document.createElement('div'));
// 观察<body>元素及其子树
observer.observe(document.body, { attributes: true, subtree: true });
// 修改<body>元素的子树
document.body.firstChild.setAttribute('foo', 'bar');
// 记录了子树变化的事件
// [
// {
// addedNodes: NodeList[],
// attributeName: "foo",
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: null,
// removedNodes: NodeList[],
// target: div,
// type: "attributes",
// }
// ]
被观察子树中的节点被移出子树之后仍然能够触发变化事件。这意味着在子树中的节点离开该子树后,即使严格来讲该节点已经脱离了原来的子树,但它仍然会触发变化事件。下面的代码演示了这种情况:
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
let subtreeRoot = document.createElement('div'),
subtreeLeaf = document.createElement('span');
// 创建包含两层的子树
document.body.appendChild(subtreeRoot);
subtreeRoot.appendChild(subtreeLeaf);
// 观察子树
observer.observe(subtreeRoot, { attributes: true, subtree: true });
// 把节点转移到其他子树
document.body.insertBefore(subtreeLeaf, subtreeRoot);
subtreeLeaf.setAttribute('foo', 'bar');
// 移出的节点仍然触发变化事件
// [MutationRecord]
MutationObserver 接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。为了在大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存MutationRecord 实例中,然后添加到记录队列 。
每次MutationRecord 被添加到MutationObserver 的记录队列时,仅当之前没有已排期的微任务回调时(队列中微任务长度为0),才会将观察者注册的回调(在初始化MutationObserver 时传入)作为微任务调度到任务队列上。
调用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]
// []
这在希望断开与观察目标的联系,但又希望处理由于调用disconnect() 而被抛弃的记录队列中的MutationRecord 实例时比较有用。
性能、内存与垃圾回收
MutationObserver 实例与目标节点之间的引用关系是非对称的。
MutationObserver 拥有对要观察的目标节点的弱引用。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。
目标节点却拥有对MutationObserver的强引用。如果目标节点从DOM中被移除,随后被垃圾回收,则关联的MutationObserver 也会被垃圾回收。
记录队列中的每个MutationRecord 实例至少包含对已有DOM节 点的一个引用。
如果变化是childList 类型,则会包含多个节点的 引用。记录队列和回调处理的默认行为是耗尽这个队列,处理每 个MutationRecord
,然后让它们超出作用域并被垃圾回收。
有时候可能需要保存某个观察者的完整变化记录。保存这些 MutationRecord实例,也就会保存它们引用的节点,因而会妨碍 这些节点被回收。
如果需要尽快地释放内存,建议从每 个MutationRecord 中抽取出最有用的信息,然后保存到一个新对 象中,最后抛弃MutationRecord 。