【红宝书笔记精简版】第十四章DOM

目录

14.1 节点层级

14.1.2 Document 类型

14.1.3 Element 类型 

14.1.4 Text 类型

14.1.5 Comment 类型

14.1.6 CDATASection 类型

14.1.7 DocumentType 类型

14.1.8 DocumentFragment 类型

14.1.9 Attr 类型

14.2 DOM 编程

14.2.1 动态脚本

14.2.2 动态样式

14.2.3 操作表格

14.2.4 使用 NodeList

14.3 MutationObserver 接口

14.3.1 基本用法

14.3.2 MutationObserverInit 与观察范围

14.3.3 异步回调与记录队列

14.3.4 性能、内存与垃圾回收

14.4 小结


文档对象模型(DOM,Document Object Model)是 HTML 和 XML 文档的编程接口。DOM 表示 由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。

14.1 节点层级

14.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)
节点类型可通过与这些常量比较来确定,比如:

if (someNode.nodeType == Node.ELEMENT_NODE){
 alert("Node is an element.");
} 

这个例子比较了 someNode.nodeType 与 Node.ELEMENT_NODE 常量。如果两者相等,则意味着 someNode 是一个元素节点。 浏览器并不支持所有节点类型。开发者最常用到的是元素节点和文本节点。本章后面会讨论每种节 点受支持的程度及其用法。

1. nodeName 与 nodeValue

nodeName 与 nodeValue 保存着有关节点的信息。这两个属性的值完全取决于节点类型。在使用 这两个属性前,最好先检测节点类型.

if (someNode.nodeType == 1){
 value = someNode.nodeName; // 会显示元素的标签名
}

对元素 而言,nodeName 始终等于元素的标签名,而 nodeValue 则始终为 null。

2. 节点关系

每个节点都有一个 childNodes 属性,其中包含一个 NodeList 的实例。NodeList 是一个类数组 对象,用于存储可以按位置存取的有序节点。注意,NodeList 并不是 Array 的实例,但可以使用中括 号访问它的值,而且它也有 length 属性。NodeList 对象独特的地方在于,它其实是一个对 DOM 结 构的查询,因此 DOM 结构的变化会自动地在 NodeList 中反映出来.

使用中括号或使用 item()方法访问 NodeList 中的元素:

let firstChild = someNode.childNodes[0];
let secondChild = someNode.childNodes.item(1);
let count = someNode.childNodes.length; 

使用 Array.prototype. slice()可以像前面介绍 arguments 时一样把 NodeList 对象转换为数组:

let arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);

使用 ES6 的 Array.from()静态方法:

let arrayOfNodes = Array.from(someNode.childNodes);

每个节点都有一个 parentNode 属性,指向其 DOM 树中的父元素。childNodes 列表中的每个 节点都是同一列表中其他节点的同胞节点。而使用 previousSibling 和 nextSibling 可以在这个列 表的节点间导航。

if (someNode.nextSibling === null){
 alert("Last node in the parent's childNodes list.");
} else if (someNode.previousSibling === null){
 alert("First node in the parent's childNodes list.");
} 

如果 childNodes 中只有一个节点,则它的 previousSibling 和 nextSibling 属性都是 null。

父节点和它的第一个及最后一个子节点也有专门属性:firstChild 和 lastChild 分别指向 childNodes 中的第一个和最后一个子节点。

 还有一个便利 的方法是 hasChildNodes(),这个方法如果返回 true 则说明节点有一个或多个子节点。

3. 操纵节点

因为所有关系指针都是只读的,所以 DOM 又提供了一些操纵节点的方法。最常用的方法是 appendChild(),用于在 childNodes 列表末尾添加节点。appendChild()方法返回新添加的节点:

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

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

// 假设 someNode 有多个子节点
let returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); // false
alert(returnedNode == someNode.lastChild); // true 

如果想把节点放到 childNodes 中的特定位置而不是末尾,则可以使用 insertBefore()方法。 这个方法接收两个参数:要插入的节点和参照节点。调用这个方法后,要插入的节点会变成参照节点的 前一个同胞节点,并被返回。如果参照节点是 null,则 insertBefore()与 appendChild()效果相同:

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

appendChild() 和 insertBefore() 在插入节点时不会删除任何已有节点。相对地, replaceChild()方法接收两个参数:要插入的节点和要替换的节点。要替换的节点会被返回并从文档 树中完全移除,要插入的节点会取而代之。

// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
// 替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild); 

要移除节点而不是替换节点,可以使用 removeChild()方法。这个方法接收一个参数,即要移除 的节点。被移除的节点会被返回:

// 删除第一个子节点
let formerFirstChild = someNode.removeChild(someNode.firstChild);
// 删除最后一个子节点
let formerLastChild = someNode.removeChild(someNode.lastChild);

上面介绍的 4 个方法(appendChild(),insertBefore(),replaceChild(),removeChild() )都用于操纵某个节点的子元素,也就是说使用它们之前必须先取得父节点(使 用前面介绍的 parentNode 属性)。并非所有节点类型都有子节点,如果在不支持子节点的节点上调用 这些方法,则会导致抛出错误。

4. 其他方法

所有节点类型还共享了两个方法。第一个是 cloneNode(),会返回与调用它的节点一模一样的节 点。cloneNode()方法接收一个布尔值参数,表示是否深复制。在传入 true 参数时,会进行深复制, 即复制节点及其整个子 DOM 树。如果传入 false,则只会复制调用该方法的节点。复制返回的节点属 于文档所有,但尚未指定父节点,所以可称为孤儿节点(orphan)。可以通过 appendChild()、 insertBefore()或 replaceChild()方法把孤儿节点添加到文档中:

<ul>
 <li>item 1</li>
 <li>item 2</li>
 <li>item 3</li>
</ul> 

如果myList保存着对这个元素的引用,则下列代码展示了使用cloneNode()方法的两种方式:

let deepList = myList.cloneNode(true);
alert(deepList.childNodes.length); // 3(IE9 之前的版本)或 7(其他浏览器)
let shallowList = myList.cloneNode(false);
alert(shallowList.childNodes.length); // 0 

最后一个方法是 normalize()。这个方法唯一的任务就是处理文档子树中的文本节点。。在节点上调用 normalize()方法会检测这个节点的所有后代,从中搜索上述两种 情形。如果发现空文本节点,则将其删除;如果两个同胞节点是相邻的,则将其合并为一个文本节点。

14.1.2 Document 类型

Document 类型是 JavaScript 中表示文档节点的类型。在浏览器中,文档对象 document 是 HTMLDocument 的实例(HTMLDocument 继承 Document),表示整个 HTML 页面。document 是 window 对象的属性,因此是一个全局对象。Document 类型的节点有以下特征:

 nodeType 等于 9;
 nodeName 值为"#document";
 nodeValue 值为 null;
 parentNode 值为 null;
 ownerDocument 值为 null;
 子节点可以是 DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction 或 Comment 类型

1. 文档子节点

documentElement 属 性,始终指向 HTML 页面中的元素。

<html>
 <body>
 </body>
</html> 

文档只有一个子节点,即元素。这个元素既可以通过 documentElement 属性获取,也可以通过 childNodes 列表访问:

let html = document.documentElement; // 取得对<html>的引用
alert(html === document.childNodes[0]); // true
alert(html === document.firstChild); // true 

作为 HTMLDocument 的实例,document 对象还有一个 body 属性,直接指向元素。因为 这个元素是开发者使用最多的元素,所以 JavaScript 代码中经常可以看到 document.body。所有主流浏览器都支持 document.documentElement 和 document.body。

一般来说,appendChild()、removeChild()和 replaceChild()方法不会用在 document 对象 上。这是因为文档类型(如果存在)是只读的,而且只能有一个 Element 类型的子节点(即, 已经存在了)。

2. 文档信息

document 作为 HTMLDocument 的实例,还有一些标准 Document 对象上所没有的属性。这些属性 提供浏览器所加载网页的信息。其中第一个属性是 title,包含元素中的文本,通常显示在浏 览器窗口或标签页的标题栏。通过这个属性可以读写页面的标题,修改后的标题也会反映在浏览器标题 栏上。

// 读取文档标题
let originalTitle = document.title;
// 修改文档标题
document.title = "New page title"; 

接下来要介绍的 3 个属性是 URL、domain 和 referrer。其中,URL 包含当前页面的完整 URL(地 址栏中的 URL),domain 包含页面的域名,而 referrer 包含链接到当前页面的那个页面的 URL。如 果当前页面没有来源,则 referrer 属性包含空字符串。所有这些信息都可以在请求的 HTTP 头部信息 中获取,只是在 JavaScript 中通过这几个属性暴露出来而已:

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

URL 跟域名是相关的。比如,如果 document.URL 是 http://www.wrox.com/WileyCDA/,则 document.domain 就是 www.wrox.com。

在这些属性中,只有 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"; // 出错!

当页面中包含来自某个不同子域的窗格()或内嵌窗格()时,设置 document.domain 是有用的。因为跨源通信存在安全隐患,所以不同子域的页面间无法通过 JavaScript 通信。此时,在每个页面上把 document.domain 设置为相同的值,这些页面就可以访问对方的 JavaScript 对象了。比如,一个加载自 www.wrox.com 的页面中包含一个内嵌窗格,其中的页面加载自 p2p.wrox.com。这两个页面的 document.domain 包含不同的字符串,内部和外部页面相互之间不能 访问对方的 JavaScript 对象。如果每个页面都把 document.domain 设置为 wrox.com,那这两个页面 之间就可以通信了。

浏览器对 domain 属性还有一个限制,即这个属性一旦放松就不能再收紧:

// 页面来自 p2p.wrox.com
document.domain = "wrox.com"; // 放松,成功
document.domain = "p2p.wrox.com"; // 收紧,错误!

3. 定位元素

getElementById()和 getElementsByTagName()就是 Document 类型提供的两个方法:获取某个或某组元素的引用,然后对它们执行某些操作。

getElementById()方法接收一个参数,即要获取元素的 ID,如果找到了则返回这个元素,如果 没找到则返回 null。参数 ID 必须跟元素在页面中的 id 属性值完全匹配,包括大小写。

<div id="myDiv">Some text</div>
可以使用如下代码取得这个元素:
let div = document.getElementById("myDiv"); // 取得对这个<div>元素的引用
但参数大小写不匹配会返回 null:
let div = document.getElementById("mydiv"); // null

如果页面中存在多个具有相同 ID 的元素,则 getElementById()返回在文档中出现的第一个元素。

getElementsByTagName()是另一个常用来获取元素引用的方法。这个方法接收一个参数,即要 获取元素的标签名,返回包含零个或多个元素的 NodeList。HTML 文档中,这个方法返回一个 HTMLCollection 对象。下面的代码会取得页面中所有的元素并返回包含它们的 HTMLCollection:

let images = document.getElementsByTagName("img"); 
alert(images.length); // 图片数量
alert(images[0].src); // 第一张图片的 src 属性
alert(images.item(0).src); // 同上

HTMLCollection 对象还有一个额外的方法 namedItem(),可通过标签的 name 属性取得某一项 的引用。

// 假设页面中包含如下的<img>元素:
<img src="myimage.gif" name="myImage">
// 那么也可以像这样从 images 中取得对这个<img>元素的引用:
let myImage = images.namedItem("myImage");
let myImage = images["myImage"]; // 同上

要取得文档中的所有元素,可以给 getElementsByTagName()传入*。在 JavaScript 和 CSS 中,* 一般被认为是匹配一切的字符。

HTMLDocument 类型上定义的获取元素的第三个方法是 getElementsByName()。顾名思义,这个 方法会返回具有给定 name 属性的所有元素。getElementsByName()方法最常用于单选按钮,因为同 一字段的单选按钮必须具有相同的 name 属性才能确保把正确的值发送给服务器:

<fieldset>
     <legend>Which color do you prefer?</legend>
     <ul>
         <li>
             <input type="radio" value="red" name="color" id="colorRed">
             <label for="colorRed">Red</label>
         </li>
         <li>
             <input type="radio" value="green" name="color" id="colorGreen">
             <label for="colorGreen">Green</label>
         </li>
         <li>
             <input type="radio" value="blue" name="color" id="colorBlue">
            <label for="colorBlue">Blue</label>
         </li>
     </ul>
</fieldset>

这里所有的单选按钮都有名为"color"的 name 属性,但它们的 ID 都不一样。这是因为 ID 是为了 匹配对应的元素,而 name 相同是为了保证只将三个中的一个值发送给服务器。

let radios = document.getElementsByName("color"); 

 4. 特殊集合

 document.anchors 包含文档中所有带 name 属性的元素。
 document.forms 包含文档中所有元素(与 document.getElementsByTagName ("form") 返回的结果相同)。
 document.images 包含文档中所有元素(与 document.getElementsByTagName ("img") 返回的结果相同)。  document.links 包含文档中所有带 href 属性的元素。

5. DOM 兼容性检测

此 hasFeature()的返回值并不可靠。目前这个方法已经被废弃,不再建议使用。

6. 文档写入

document 对象有一个古老的能力,即向网页输出流中写入内容。这个能力对应 4 个方法:write()、 writeln()、open()和 close()。其中,write()和 writeln()方法都接收一个字符串参数,可以将 这个字符串写入网页中。write()简单地写入文本,而 writeln()还会在字符串末尾追加一个换行符 (\n)。这两个方法可以用来在页面加载期间向页面中动态添加内容。

注意 严格的 XHTML 文档不支持文档写入。对于内容类型为 application/xml+xhtml 的页面,这些方法不起作用。

14.1.3 Element 类型 

Element 类型的节点具有以下特征:

 nodeType 等于 1;
 nodeName 值为元素的标签名;
 nodeValue 值为 null;
 parentNode 值为 Document 或 Element 对象;
 子节点可以是 Element、Text、Comment、ProcessingInstruction、CDATASection、 EntityReference 类型

<div id="myDiv"></div>
// 可以像这样取得这个元素的标签名:
let div = document.getElementById("myDiv");
alert(div.tagName); // "DIV"
alert(div.tagName == div.nodeName); // true 

例子中的元素标签名为 div,ID 为"myDiv"。注意,div.tagName 实际上返回的是"DIV"而不是 "div"。在 HTML 中,元素标签名始终以全大写表示; 

1. HTML 元素

所有 HTML 元素上都有 的标准属性:

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

2. 取得属性

与属性相关的 DOM 方法 主要有 3 个:getAttribute()、setAttribute()和 removeAttribute()。这些方法主要用于操纵 属性,包括在 HTMLElement 类型上定义的属性。。如果给定的属性不存在,则 getAttribute() 返回 null。 getAttribute()方法也能取得不是 HTML 语言正式属性的自定义属性的值。比如下面的元素:

<div id="myDiv" my_special_attribute="hello!"></div>

这个元素有一个自定义属性 my_special_attribute,值为"hello!"。可以像其他属性一样使用 getAttribute()取得这个属性的值:

let value = div.getAttribute("my_special_attribute");

注意,属性名不区分大小写,因此"ID"和"id"被认为是同一个属性。

<div id="myDiv" align="left" my_special_attribute="hello"></div>

因为 id 和 align 在 HTML 中是元素公认的属性,所以 DOM 对象上也会有这两个属性。但 my_special_attribute 是自定义属性,因此不会成为 DOM 对象的属性。

3. 设置属性

与 getAttribute()配套的方法是 setAttribute(),这个方法接收两个参数:要设置的属性名 和属性的值。如果属性已经存在,则 setAttribute()会以指定的值替换原来的值;如果属性不存在, 则 setAttribute()会以指定的值创建该属性。

setAttribute()适用于 HTML 属性,也适用于自定义属性。另外,使用 setAttribute()方法 设置的属性名会规范为小写形式,因此"ID"会变成"id"。

注意,在 DOM 对象上添加自定义属性,如下面的例子所示,不会自动让它变成元素的属性:

div.mycolor = "red";
alert(div.getAttribute("mycolor")); // null(IE 除外)

最后一个方法 removeAttribute()用于从元素中删除属性。这样不单单是清除属性的值,而是会 把整个属性完全从元素中去掉:

 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()方法创建新元素。这个方法接收一个参数,即要创建元素 的标签名。

let div = document.createElement("div");
div.id = "myNewDiv";
div.className = "box"; 

在新元素上设置这些属性只会附加信息。因为这个元素还没有添加到文档树,所以不会影响浏览器 显示。要把元素添加到文档树,可以使用 appendChild()、insertBefore()或 replaceChild()。 比如,以下代码会把刚才创建的元素添加到文档的元素中:

document.body.appendChild(div);

6. 元素后代 

。childNodes 属性包含元素所有的子节点,这些子节点可能是其他元素、文本节点、注释或处理指令。不同浏览器在 识别这些节点时的表现有明显不同。

<ul id="myList">
 <li>Item 1</li>
 <li>Item 2</li>
 <li>Item 3</li>
</ul>

在解析以上代码时,元素会包含 7 个子元素,其中 3 个<li>是元素,还有 4 个 Text 节点(表 示<li>元素周围的空格)。如果把元素之间的空格删掉,变成下面这样,则所有浏览器都会返回同样数 量的子节点:

<ul id="myList"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>

所有浏览器解析上面的代码后,<li>元素都会包含 3 个子节点。考虑到这种情况,通常在执行某个 操作之后需要先检测一下节点的 nodeType,如下所示:

for (let i = 0, len = element.childNodes.length; i < len; ++i) {
 if (element.childNodes[i].nodeType == 1) {
 // 执行某个操作
 }
} 
//可以像下面这样取得其所有的<li>元素:
let ul = document.getElementById("myList");
let items = ul.getElementsByTagName("li");

14.1.4 Text 类型

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

 nodeType 等于 3;
 nodeName 值为"#text";
 nodeValue 值为节点中包含的文本;
 parentNode 值为 Element 对象;
 不支持子节点。

默认情况下,包含文本内容的每个元素最多只能有一个文本节点。例如:

<!-- 没有内容,因此没有文本节点 -->
<div></div>
<!-- 有空格,因此有一个文本节点 -->
<div> </div>
<!-- 有内容,因此有一个文本节点 -->
<div>Hello World!</div> 
// 下列代码可以用来访问这个文本节点:
let textNode = div.firstChild; // 或 div.childNodes[0]
// 取得文本节点的引用后,可以像这样来修改它:
div.firstChild.nodeValue = "Some other message";

 1. 创建文本节点

document.createTextNode()可以用来创建新文本节点,它接收一个参数,即要插入节点的文本。 跟设置已有文本节点的值一样,这些要插入的文本也会应用 HTML 或 XML 编码。

创建新文本节点后,其 ownerDocument 属性会被设置为 document。但在把这个节点添加到文档 树之前,我们不会在浏览器中看到它:

let element = document.createElement("div");
element.className = "message";
let textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element); 

2. 规范化文本节点 

DOM 文档中也经常会出现两个相邻文本节点。为此,有一个方法可以合并相邻的文本节点。这个方法 叫 normalize(),是在 Node 类型中定义的。在包含两个或多 个相邻文本节点的父节点上调用 normalize()时,所有同胞文本节点会被合并为一个文本节点。浏览器在解析文档时,永远不会创建同胞文本节点。同胞文本节点只会出现在 DOM 脚本生成的文 档树中。

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

14.1.5 Comment 类型

DOM 中的注释通过 Comment 类型表示。Comment 类型的节点具有以下特征:

 nodeType 等于 8;
 nodeName 值为"#comment";
 nodeValue 值为注释的内容;
 parentNode 值为 Document 或 Element 对象;
 不支持子节点。

与 Text 类型相似,注释的实际内容可以通过 nodeValue 或 data 属性获得。

<div id="myDiv"><!-- A comment --></div>
// 这里的注释是<div>元素的子节点,这意味着可以像下面这样访问它:
let div = document.getElementById("myDiv");
let comment = div.firstChild;
alert(comment.data); // "A comment"
alert(comment.nodeValue); // "A comment"

14.1.6 CDATASection 类型

CDATASection 类型表示 XML 中特有的 CDATA 区块。CDATASection 类型的节点具有以下特征:  nodeType 等于 4;
 nodeName 值为"#cdata-section";
 nodeValue 值为 CDATA 区块的内容;
 parentNode 值为 Document 或 Element 对象;
 不支持子节点。

14.1.7 DocumentType 类型

DocumentType 类型的节点包含文档的文档类型(doctype)信息,具有以下特征:
 nodeType 等于 10;
 nodeName 值为文档类型的名称;
 nodeValue 值为 null;
 parentNode 值为 Document 对象;
 不支持子节点。

14.1.8 DocumentFragment 类型

在所有节点类型中,DocumentFragment 类型是唯一一个在标记中没有对应表示的类型。DOM 将 文档片段定义为“轻量级”文档,能够包含和操作节点,却没有完整文档那样额外的消耗。DocumentFragment 节点具有以下特征:
 nodeType 等于 11;
 nodeName 值为"#document-fragment";
 nodeValue 值为 null;
 parentNode 值为 null;
 子节点可以是 Element、ProcessingInstruction、Comment、Text、CDATASection 或 EntityReference。

不能直接把文档片段添加到文档。相反,文档片段的作用是充当其他要被添加到文档的节点的仓库。 可以使用 document.createDocumentFragment()方法像下面这样创建文档片段:

let fragment = document.createDocumentFragment(); 

文档片段从 Node 类型继承了所有文档类型具备的可以执行 DOM 操作的方法。如果文档中的一个 节点被添加到一个文档片段,则该节点会从文档树中移除,不会再被浏览器渲染。添加到文档片段的新 节点同样不属于文档树,不会被浏览器渲染。可以通过 appendChild()或 insertBefore()方法将文 档片段的内容添加到文档。在把文档片段作为参数传给这些方法时,这个文档片段的所有子节点会被添 加到文档中相应的位置。文档片段本身永远不会被添加到文档树。 

<ul id="myList"></ul>

假设想给这个<li>元素添加 3 个列表项。如果分 3 次给这个元素添加列表项,浏览器就要重新渲染 3 次页面,以反映新添加的内容。为避免多次渲染,下面的代码示例使用文档片段创建了所有列表项, 然后一次性将它们添加到<li>元素

let fragment = document.createDocumentFragment();
let ul = document.getElementById("myList");
for (let i = 0; i < 3; ++i) {
 let li = document.createElement("li");
 li.appendChild(document.createTextNode(`Item ${i + 1}`));
 fragment.appendChild(li);
}
ul.appendChild(fragment);

14.1.9 Attr 类型

元素数据在 DOM 中通过 Attr 类型表示。Attr 类型构造函数和原型在所有浏览器中都可以直接访 问。技术上讲,属性是存在于元素 attributes 属性中的节点。Attr 节点具有以下特征:

 nodeType 等于 2;
 nodeName 值为属性名;
 nodeValue 值为属性值;
 parentNode 值为 null;
 在 HTML 中不支持子节点;
 在 XML 中子节点可以是 Text 或 EntityReference。

属性节点尽管是节点,却不被认为是 DOM 文档树的一部分。Attr 节点很少直接被引用,通常开 发者更喜欢使用 getAttribute()、removeAttribute()和 setAttribute()方法操作属性。

注意 将属性作为节点来访问多数情况下并无必要。推荐使用 getAttribute()、 removeAttribute()和 setAttribute()方法操作属性,而不是直接操作属性节点。

14.2 DOM 编程

14.2.1 动态脚本

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

动态加载外部文件很容易实现,比如:

<script src="foo.js"></script>

可以像这样通过 DOM 编程创建这个节点:

let script = document.createElement("script");
script.src = "foo.js";
document.body.appendChild(script);

14.2.2 动态样式

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

<link rel="stylesheet" type="text/css" href="styles.css"> 

注意应该把<link>元素添加到<head>元素而不是<body>元素,这样才能保证所有浏览器都能正常运行。 

14.2.3 操作表格

14.2.4 使用 NodeList

理解 NodeList 对象和相关的 NamedNodeMap、HTMLCollection,是理解 DOM 编程的关键。这 3 个集合类型都是“实时的”,意味着文档结构的变化会实时地在它们身上反映出来,实际上,NodeList 就是基于 DOM 文档的实时查询。例如,下面的代码会导致无穷 循环:

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

因为循环 体中会创建并向文档添加一个新<div>元素,所以每次循环 divs.length 的值也会递增。因为两个值 都会递增,所以 i 将永远不会等于 divs.length。

 任何时候要迭代 NodeList,最好再初始化一个变量保存当时查询时的长度,然后用循环变量与这 个变量进行比较:

for (let i = 0, len = divs.length; i < len; ++i)

 另外,如果不想再初始化一个变量,也可以像下面这样反向迭代集合:

for (let i = divs.length - 1; i >= 0; --i)

14.3 MutationObserver 接口

使 用 MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。

注意 新引进 MutationObserver 接口是为了取代废弃的 MutationEvent。

14.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 });

执行以上代码后,元素上任何属性发生变化都会被这个 MutationObserver 实例发现,然 后就会异步执行注册的回调函数。元素后代的修改或其他非属性修改都不会触发回调进入任务 队列。

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 实例的数组:

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

连续修改会生成多个 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] 

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';
//(没有日志输出)

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 });
// 修改两个子节点的属性
childA.setAttribute('foo', 'bar');
childB.setAttribute('foo', 'bar');
// [<div>, <span>] 

14.3.2 MutationObserverInit 与观察范围

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

注意 在调用 observe()时,MutationObserverInit 对象中的 attribute、characterData 和 childList 属性必须至少有一项为 true(无论是直接设置这几个属性,还是通过设置 attributeOldValue 等属性间接导

1. 观察属性

observer.observe(document.body, { attributes: true });

2. 观察字符数据 

observer.observe(document.body.firstChild, { characterDataOldValue: true });

3. 观察子节点

observer.observe(document.body, { childList: true });

4. 观察子树

// 观察<body>元素及其子树
observer.observe(document.body, { attributes: true, subtree: true });

14.3.3 异步回调与记录队列

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

1. 记录队列

不过在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一 个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。

2. takeRecords()方法

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

14.3.4 性能、内存与垃圾回收

将变化回调委托给微任务来执行可以保证事件同步触发,同时避免随之而来的混乱。为 MutationObserver 而实现的记录队列,可以保证即使变化事件被爆发式地触发,也不会显著地拖慢浏览器。

1. MutationObserver 的引用

MutationObserver 实例与目标节点之间的引用关系是非对称的。MutationObserver 拥有对要 观察的目标节点的弱引用。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。

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

14.4 小结

文档对象模型(DOM,Document Object Model)是语言中立的 HTML 和 XML 文档的 API。DOM Level 1 将 HTML 和 XML 文档定义为一个节点的多层级结构,并暴露出 JavaScript 接口以操作文档的底 层结构和外观。

DOM 由一系列节点类型构成,主要包括以下几种。
 Node 是基准节点类型,是文档一个部分的抽象表示,所有其他类型都继承 Node。
 Document 类型表示整个文档,对应树形结构的根节点。在 JavaScript 中,document 对象是 Document 的实例,拥有查询和获取节点的很多方法。
 Element 节点表示文档中所有 HTML 或 XML 元素,可以用来操作它们的内容和属性。
 其他节点类型分别表示文本内容、注释、文档类型、CDATA 区块和文档片段。 DOM 编程在多数情况下没什么问题,在涉及<style>元素和<script>元素时会有一点兼容性问题。因 为这些元素分别包含脚本和样式信息,所以浏览器会将它们与其他元素区别对待。

要理解 DOM,最关键的一点是知道影响其性能的问题所在。DOM 操作在 JavaScript 代码中是代价 比较高的,NodeList 对象尤其需要注意。NodeList 对象是“实时更新”的,这意味着每次访问它都 会执行一次新的查询。考虑到这些问题,实践中要尽量减少 DOM 操作的数量。 MutationObserver 是为代替性能不好的 MutationEvent 而问世的。使用它可以有效精准地监控 DOM 变化,而且 API 也相对简单。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值