目录
14.3.2 MutationObserverInit 与观察范围
文档对象模型(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 也相对简单。