JavaScript高级程序设计第四版学习--第十四章


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 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值