最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 红宝书之第十四章: DOM

    正文概述 掘金(梁少杰)   2021-01-02   345

    文档对象模型(DOM,Document Object Model)是 HTML 和 XML 文档的编程接口。

    节点层级

    根节点的唯一子节点是元素,我们称之 为文档元素(documentElement)。文档元素是文档最外层的元素,所有其他元素都存在于这个元素之 内。每个文档只能有一个文档元素。在 HTML 页面中,文档元素始终是元素。

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

    1. nodeName 与 nodeValue

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

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

    2. 节点关系

    总的来说就两种:==父子关系,兄弟关系==

    • 父节点访问子节点:childNodes
    • 子节点访问父节点:parentNode
    • 上一个兄弟节点:previousSibling
    • 下一个兄弟节点:nextSibling
    • 父节点访问第一个子节点:firstChild
    • 父节点访问最后一个子节点:lastChild

    注意点:NodeList 并不是 Array 的实例,但可以使用中括 号访问它的值,而且它也有 length 属性。

    判读有没有子节点还可以用:hasChildNodes(),这个方法如果返回 true 则说明节点有一个或多个子节点。

    ownerDocument 属性是一个指向代表整个文档的文档节点 的指针。

    3. 操纵节点

    因为所有关系指针都是只读的,所以 DOM 又提供了一些操纵节点的方法

    • appendChild() 用于在 childNodes 列表末尾添加节点。
    • insertBefore() 把节点放到 childNodes 中的特定位置而不是末尾,接收两个参数:要插入的节点和参照节点
    • replaceChild() 要替换的节点会被返回并从文档

    树中完全移除,要插入的节点会取而代之,接收两个参数:要插入的节点和要替换的节点。

    • removeChild() 这个方法接收一个参数,即要移除

    的节点。被移除的节点会被返回

    注意点: appendChild()方法返回新添加的节点,

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

    如果把文档中已经存在的节点传给 appendChild(),则这个节点会从之前的位置被转移到新位置。

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

    如果参照节点是 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
    

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

    4. 其他方法

    • cloneNode() 会返回与调用它的节点一模一样的节

    点。cloneNode()方法接收一个布尔值参数,表示是否深复制。在传入 true 参数时,会进行深复制, 即复制节点及其整个子 DOM 树。如果传入 false,则只会复制调用该方法的节点。

    • normalize() 这个方法唯一的任务就是处理文档子树中的文本节

    点。如果发现空文本节点,则将其删除;如果两个同胞节点是相邻的,则将其合并为一个文本节点。

    Document 类型

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

    文档子节点

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

    三个便捷属性:

    • documentElement
    • body
    • doctype

    浏览器解析完这个页面之后,文档只有一个子节点,即元素。这个元素既可以通过documentElement 属性获取,也可以通过 childNodes 列表访问,如下所示:

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

    这个例子表明 documentElement、firstChild 和 childNodes[0]都指向同一个值,即 元素。

    document 对象还有一个 body 属性,直接指向元素。因为 这个元素是开发者使用最多的元素,所以 JavaScript 代码中经常可以看到 document.body

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

    Document 类型另一种可能的子节点是 DocumentType。标签是文档中独立的部分, 其信息可以通过 doctype 属性(在浏览器中是 document.doctype)来访问,

    let doctype = document.doctype; // 取得对<!doctype>的引用
    

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

    文档信息

    • title
    • URL
    • domain
    • referrer

    其中最常用的是title,包含元素中的文本,通常显示在浏 览器窗口或标签页的标题栏。通过这个属性可以读写页面的标题,修改后的标题也会反映在浏览器标题 栏上。

    ==使用domain是如何实现不同源网站相互通信?==

    定位元素

    • getElementById()
    • getElementsByTagName()
    • getElementsByName()

    注意点:

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

    getElementsByTagName这个方法接收一个参数,即要 获取元素的标签名,返回包含零个或多个元素的 HTMLCollection。

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

    数值索引会调用 item(),字符串索引会调用 namedItem()。

    要取得文档中的所有元素,可以给 getElementsByTagName()传入*。

    getElementsByName这个 方法会返回具有给定 name 属性的所有元素。getElementsByName()方法最常用于单选按钮,因为同一字段的单选按钮必须具有相同的 name 属性才能确保把正确的值发送给服务器

    特殊集合

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

    • document.anchors
    • document.applets
    • document.forms
    • document.images
    • document.links

    具体用法看书上讲解;

    DOM 兼容性检测

    document.implementation 属性是一个对象,其中提供了与浏览器 DOM 实现相关的信息和能力。

    hasFeature()。这个方法接 收两个参数:特性名称和 DOM 版本。如果浏览器支持指定的特性和版本,则 hasFeature()方法返回 true

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

    文档写入

    write()和 writeln()方法都接收一个字符串参数,可以将 这个字符串写入网页中。write()简单地写入文本,而 writeln()还会在字符串末尾追加一个换行符 (\n)。这两个方法可以用来在页面加载期间向页面中动态添加内容

    <html>
    <head>
        <title>document.write() Example</title>
    </head>
    <body>
        <script type="text/javascript">
            document.write("<script type=\"text/javascript\" src=\"file.js\">" +
                "<\/script>"); 
        </script>
    </body>
    
    </html>
    

    前面的例子展示了在页面渲染期间通过 document.write()向文档中输出内容。如果是在页面加 载完之后再调用 document.write(),则输出的内容会重写整个页面

    <html>
    
    <head>
        <title>document.write() Example</title>
    </head>
    
    <body>
        <p>This is some content that you won't get to see because it will be
            overwritten.</p>
        <script type="text/javascript">
            window.onload = function () {
                document.write("Hello world!");
            }
        </script>
    </body>
    
    </html>
    
    

    这个例子使用了 window.onload 事件处理程序,将调用 document.write()的函数推迟到页面 加载完毕后执行。执行之后,字符串"Hello world!"会重写整个页面内容

    Element 类型

    nodeName 或 tagName 属性来获取元素的标签名

    <div id="myDiv"></div>
    

    可以像这样取得这个元素的标签名:

    let div = document.getElementById("myDiv"); 
    alert(div.tagName); // "DIV" 
    alert(div.tagName == div.nodeName); // true
    

    如果不确定脚本是在 HTML 文档还是 XML 文档中运行,最好将标签名转换为小 写形式,以便于比较:

    if (element.tagName == "div"){ // 不要这样做,可能出错!
     // do something here 
    } 
    if (element.tagName.toLowerCase() == "div"){ // 推荐,适用于所有文档
     // 做点什么
    }
    

    HTML元素

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

    属性相关

    与属性相关的 DOM 方法主要有 3 个:

    • getAttribute()
    • setAttribute()
    • removeAttribute()

    取得属性

    两种方式可以取得:

    • 1,getAttribute()
    • 2,DOM对象上取得

    比如:

    定义了一个div

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

    就可以通过下面方法取得属性:

    let div = document.getElementById("myDiv"); 
    alert(div.getAttribute("id")); // "myDiv"
    alert(div.getAttribute("id")); // "myDiv"
    alert(div.id)   // "myDiv"
    // 注意这个
    alert(div.ID)   // undefined
    
    // 自定义属性
    alert(div.getAttribute("my_special_attribute"))  // hello
    

    值得注意的是:

    getAttribute()的属性名与它们实际的属性名是一样的,因此这里要传"class"而非"className"

    如果给定的属性不存在,则 getAttribute() 返回 null

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

    getAttribute()方法也能取得不是HTML语言正式属性的自定义属性的值。

    根据 HTML5 规范的 要求,自定义属性名应该前缀 data-以方便验证。

    通过 DOM 对象访问的属性中有两个返回的值跟使用 getAttribute()取得的值不一样,一个是style属性,另一个是事件处理程序;

    • style属性:在使用 getAttribute()访问 style 属性时,返回的

    是 CSS 字符串。而在通过 DOM 对象的属性访问时,style 属性返回的是一个(CSSStyleDeclaration) 对象。

    • 事件处理程序:如果使用 getAttribute()访问事件属性,

    则返回的是字符串形式的源代码。而通过 DOM 对象的属性访问事件属性时返回的则是一个 JavaScript 函数(未指定该属性则返回 null)。

    考虑到以上差异,开发者在进行DOM编程时通常会放弃使用getAttribute()而只使用对象属性。getAttribute()主要用于取得自定义属性的值

    设置属性

    同样两种方式,一个是setAttribute,一个是dom对象操作;

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

    注意点:

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

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

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

    删除属性

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

    attributes 属性

    相当于是多个属性的集合

    •  getNamedItem(name),返回 nodeName 属性等于 name 的节点;
    •  removeNamedItem(name),删除 nodeName 属性等于 name 的节点;
    •  setNamedItem(node),向列表中添加 node 节点,以其 nodeName 为索引;
    •  item(pos),返回索引位置 pos 处的节点。

    因为使用起来更简便,通常开发者更喜欢使用 getAttribute()、removeAttribute() 和 setAttribute()方法,

    attributes 属性最有用的场景是需要迭代元素上所有属性的时候。

    比如,以下代码能够迭代一个元素上的所有属性并以 attribute1= "value1" attribute2="value2"的形式生成格式化字符串:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="myDiv" class="bd"  a="b" lang="en" style="color:red;" dir="ltr">12345</div>
        <script>
            let div = document.getElementById("myDiv");
    
            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(" ");
            }
    
            console.log(outputAttributes(div)) // id="myDiv" class="bd"  a="b" lang="en" style="color:red;" dir="ltr"
    
        </script>
    </body>
    
    </html>
    

    不同浏览器返回的 attributes 中的属性顺序也可能不一样。 HTML 或 XML 代码中属性出现的顺序不一定与 attributes 中的顺序一致。

    创建元素

    document.createElement()方法创建新元素。这个方法接收一个参数,即要创建元素的标签名。

    要创建

    元素,可以使用下面的代码:
    let div = document.createElement("div");
    

    使用 createElement()方法创建新元素的同时也会将其 ownerDocument 属性设置为 document。

    此时,可以再为其添加属性、添加更多子元素

    div.id = "myNewDiv"; 
    div.className = "box";
    

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

    document.body.appendChild(div);
    

    元素被添加到文档树之后,浏览器会立即将其渲染出来。之后再对这个元素所做的任何修改,都会立即在浏览器中反映出来。

    元素后代

    childNodes 属性包含元素所有的子节点,这些子节点可能是其他元素、文本节点、注释或处理指令。

    注意空格文本元素

    开发者正常的写法:

    <ul id="myList"> 
     <li>Item 1</li> 
     <li>Item 2</li> 
     <li>Item 3</li> 
    </ul>
    <!--<ul>元素会包含 7 个子元素,其中 3 个是<li>元素,还有 4 个 Text 节点(表示<li>元素周围的空格)。-->
    

    经过压缩代码之后就会变成下面的

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

    考虑到这种情况,通常在执行某个 操作之后需要先检测一下节点的 nodeType,如下所示:

    for (let i = 0, len = element.childNodes.length; i < len; ++i) { 
     if (element.childNodes[i].nodeType == 1) { 
     // 执行某个操作
     } 
    }
    

    含有一种方式:可以使用元素的 getElementsByTagName()方法。在 元素上调用这个方法与在文档上调用是一样的,只不过搜索范围限制在当前元素之内,即只会返回当前元素的后代。如果它包含更多层级,则所有层级中的<li>元素都会返回

    Text 类型

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

    Text 节点中包含的文本可以通过 nodeValue 属性访问,也可以通过 data 属性访问,这两个属性包含相同的值。修改 nodeValue 或 data 的值,也会在另一个属性反映出来。

    •  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 的文本。

    访问文本节点:

    let textNode = div.firstChild; // 或 div.childNodes[0]
    

    修改文本节点:

    div.firstChild.nodeValue = "Some other message";
    

    需要注意,就是HTML 或 XML 代码(取决于文档类型)会被转换成实体编码,即小于号、大于号或引号会被转义,如下所示:

    // 输出为"Some &lt;strong&gt;other&lt;/strong&gt; message" 
    div.firstChild.nodeValue = "Some <strong>other</strong> message";
    

    实际上用innerHTML方法更多一些;

    创建文本节点

    document.createTextNode()可以用来创建新文本节点,它接收一个参数,即要插入节点的文本。

    创建文本节点并且把它插入到文档中:

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

    这个例子首先创建了一个

    元素并给它添加了值为"message"的 class 属性,然后又创建了一个文本节点并添加到该元素。最后一步是把这个元素添加到文档的主体上,这样元素及其包含的文本会出现在浏览器中。

    一般来说一个元素只包含一个文本子节点。不过,也可以让元素包含多个文本子节点,如下面的例子所示:

    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);
    

    在将一个文本节点作为另一个文本节点的同胞插入后,两个文本节点的文本之间不会包含空格。

    规范化文本节点

    在包含两个或多个相邻文本节点的父节点上调用 normalize()时,所有同胞文本节点会被合并为一个文本节点,这个文本节点的 nodeValue 就等于之前所有同胞节点 nodeValue 拼接在一起得到的字符串。

    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!"
    

    拆分文本节点

    Text 类型定义了一个与 normalize()相反的方法——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
    

    拆分文本节点最常用于从文本节点中提取数据的 DOM 解析技术。

    Comment 类型

    DOM 中的注释通过 Comment 类型表示。

    Comment 类型与 Text 类型继承同一个基类(CharacterData),因此拥有除 splitText()之外Text 节点所有的字符串操作方法。与 Text 类型相似,注释的实际内容可以通过 nodeValue 或 data属性获得。

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

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

    这里的注释是

    元素的子节点,这意味着可以像下面这样访问它:
    let div = document.getElementById("myDiv"); 
    let comment = div.firstChild; 
    alert(comment.data); // "A comment"
    

    可以使用 document.createComment()方法创建注释节点,参数为注释文本,很少用;

    CDATASection 类型(了解)

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

    用法:

    <div id="myDiv"><![CDATA[This is some content.]]></div>
    

    这里

    的第一个子节点应该是 CDATASection 节点。但主流的四大浏览器没有一个将其识别为CDATASection。即使在有效的 XHTML 文档中,这些浏览器也不能恰当地支持嵌入的 CDATA 区块。

    在真正的 XML 文档中,可以使用 document.createCDataSection()并传入节点内容来创建CDATA 区块。

    DocumentType 类型(了解)

    DocumentType 类型的节点包含文档的文档类型(doctype)信息,nodeType 等于 10,不支持子节点。

    DOM Level 1 规定了DocumentType 对象的 3 个属性:name、entities 和 notations。其中,name 是文档类型的名称,entities 是这个文档类型描述的实体的 NamedNodeMap,而 notations 是这个文档类型描述的表示法的 NamedNodeMap。因为浏览器中的文档通常是 HTML 或 XHTML 文档类型,所以 entities 和notations 列表为空。

    只有 name 属性是有用的。这个属性包含文档类型的名称,即紧跟在<!DOCTYPE 后面的那串文本。

    比如下面的 HTML 4.01 严格文档类型:

    <!DOCTYPE HTML PUBLIC "-// W3C// DTD HTML 4.01// EN" 
     "http:// www.w3.org/TR/html4/strict.dtd">
    

    对于这个文档类型,name 属性的值是"html":

    alert(document.doctype.name); // "html"
    

    DocumentFragment 类型

    在所有节点类型中,DocumentFragment 类型是唯一一个在标记中没有对应表示的类型。DOM 将文档片段定义为“轻量级”文档,能够包含和操作节点,却没有完整文档那样额外的消耗。

    DocumentFragment 节点具有以下特征:

    •  nodeType 等于 11;
    •  nodeName 值为"#document-fragment";
    •  nodeValue 值为 null;
    •  parentNode 值为 null;
    •  子节点可以是 Element、ProcessingInstruction、Comment、Text、CDATASection 或
    • EntityReference。

    出现目的:为避免多次渲染

    假设想给这个

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

        Attr 类型(了解)

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

        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"
        

        attributes 属性和getAttributeNode()方法都返回属性对应的 Attr 节点,而 getAttribute()方法只返回属性的值。开发中用的不多;

        DOM 编程

        动态脚本

        动态脚本就是在页面初始加载时不存在,之后又通过 DOM 包含的脚本。

        有两种方式通过

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

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

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

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

        这个过程可以抽象为一个函数,比如:

        function loadScript(url) { 
         let script = document.createElement("script"); 
         script.src = url; 
         document.body.appendChild(script); 
        }
        // 然后,就可以像下面这样加载外部 JavaScript 文件了:
        loadScript("client.js");
        

        另一个动态插入 JavaScript 的方式是嵌入源代码

        <script> 
           function sayHi() { 
           		alert("hi");
           } 
        </script>
        
        let script = document.createElement("script"); 
        script.appendChild(document.createTextNode("function sayHi(){alert('hi');}")); 
        document.body.appendChild(script);
        

        以上代码可以在 Firefox、Safari、Chrome 和 Opera 中运行。不过在旧版本的 IE 中可能会导致问题。这是因为 IE 对

        var script = document.createElement("script"); 
        script.text = "function sayHi(){alert('hi');}"; 
        document.body.appendChild(script);
        

        这样修改后,上面的代码可以在 IE、Firefox、Opera 和 Safari 3 及更高版本中运行。Safari 3 之前的版本不能正确支持这个 text 属性,但这些版本却支持文本节点赋值。

        我们就可以抽象出一个跨浏览器的函数:

        function loadScriptString(code){ 
         var script = document.createElement("script"); 
         script.type = "text/javascript"; 
         try { 
         script.appendChild(document.createTextNode(code)); 
         } catch (ex){ 
         script.text = code; 
         } 
         document.body.appendChild(script); 
        }
        

        这个函数可以这样调用:

        loadScriptString("function sayHi(){alert('hi');}");
        

        以这种方式加载的代码会在全局作用域中执行,并在调用返回后立即生效。基本上,这就相当于在全局作用域中把源代码传给 eval()方法。

        注意,通过 innerHTML 属性创建的

        动态样式

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

        来看下面这个典型的元素:

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

        这个元素很容易使用 DOM 编程创建出来:

        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);
        

        以上代码在所有主流浏览器中都能正常运行。注意应该把元素添加到元素而不是元素,这样才能保证所有浏览器都能正常运行。

        function loadStyles(url){ 
         let link = document.createElement("link"); 
         link.rel = "stylesheet"; 
         link.type = "text/css"; 
         link.href = url; 
         let head = document.getElementsByTagName("head")[0]; 
         head.appendChild(link); 
        }
        

        然后就可以这样调用这个 loadStyles()函数了:

        loadStyles("styles.css");
        

        通过外部文件加载样式是一个异步过程。

        另一种定义样式的方式是使用

        <style type="text/css"> 
        body { 
         background-color: red; 
        } 
        </style>
        

        逻辑上,下列 DOM 代码会有同样的效果:

        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);
        

        以上代码在 Firefox、Safari、Chrome 和 Opera 中都可以运行,但 IE 除外。IE 对节点会施加限制,不允许访问其子节点,这一点与它对元素施加的限制一样。事实上,IE 在执行到给<style>添加子节点的代码时,会抛出与给<script>添加子节点时同样的错误。

        对于 IE,解决方案是访问元素的 styleSheet 属性,这个属性又有一个 cssText 属性,然后给这个属性添加 CSS 代码:

        let style = document.createElement("style"); 
        style.type = "text/css"; 
        try{ 
         style.appendChild(document.createTextNode("body{background-color:red}")); 
        } catch (ex){ 
        style.styleSheet.cssText = "body{background-color:red}"; 
        } 
        let head = document.getElementsByTagName("head")[0]; 
        head.appendChild(style);
        

        与动态添加脚本源代码类似,这里也使用了 try...catch 语句捕获 IE 抛出的错误,然后再以 IE特有的方式来设置样式。这是最终的通用函数:

        function loadStyleString(css){ 
         let style = document.createElement("style"); 
         style.type = "text/css"; 
         try{ 
         style.appendChild(document.createTextNode(css)); 
         } catch (ex){ 
         style.styleSheet.cssText = css; 
         } 
         let head = document.getElementsByTagName("head")[0]; 
         head.appendChild(style); 
        }
        

        可以这样调用这个函数:

        loadStyleString("body{background-color:red}");
        

        这样添加的样式会立即生效,因此所有变化会立即反映出来。

        操作表格

        表格是 HTML 中最复杂的结构之一。通过 DOM 编程创建

        元素,通常要涉及大量标签,包括表行、表元、表题,等等。因此,通过 DOM 编程创建和修改表格时可能要写很多代码。假设要通过DOM 来创建以下 HTML 表格:
        <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>
        

        下面就是以 DOM 编程方式重建这个表格的代码:

        // 创建表格
        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);
        

        以上代码相当烦琐,也不好理解。为了方便创建表格,HTML DOM 给

        、和元素添加了一些属性和方法。

        table元素添加了以下属性和方法:

        • caption,指向caption元素的指针(如果存在);
        • tBodies,包含tbody元素的 HTMLCollection;
        • tFoot,指向tfoot元素(如果存在);
        • Head,指向thead元素(如果存在);
        • rows,包含表示所有行的 HTMLCollection;
        • createTHead(),创建thead元素,放到表格中,返回引用;
        • createTFoot(),创建tfoot元素,放到表格中,返回引用;
        • createCaption(),创建caption元素,放到表格中,返回引用;
        • deleteTHead(),删除thead元素;
        • deleteTFoot(),删除tfoot元素;
        • deleteCaption(),删除caption元素;
        • deleteRow(pos),删除给定位置的行;
        • insertRow(pos),在行集合中给定位置插入一行。
        元素添加了以下属性和方法:
        • rows,包含
        元素中所有行的 HTMLCollection;
      • deleteRow(pos),删除给定位置的行;
      • insertRow(pos),在行集合中给定位置插入一行,返回该行的引用。
      • 元素添加了以下属性和方法:
        • cells,包含
        元素所有表元的 HTMLCollection;
      • deleteCell(pos),删除给定位置的表元;
      • insertCell(pos),在表元集合给定位置插入一个表元,返回该表元的引用。
      • 这些属性和方法极大地减少了创建表格所需的代码量。

        使用 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); 
        }
        

        使用 ES6 迭代器并不会解决这个问题,因为迭代的是一个永远增长的实时集合。以下代码仍然会导致无穷循环:

        for (let div of document.getElementsByTagName("div")){ 
         let newDiv = document.createElement("div"); 
         document.body.appendChild(newDiv); 
        }
        

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

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

        在这个例子中,又初始化了一个保存集合长度的变量 len。因为 len 保存着循环开始时集合的长度,而这个值不会随集合增大动态增长,所以就可以避免前面例子中出现的无穷循环。

        一般来说,最好限制操作 NodeList 的次数。因为每次查询都会搜索整个文档,所以最好把查询到的 NodeList 缓存起来。

        MutationObserver 接口

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

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

        基本用法

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

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

        observe()方法

        新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observer 与 DOM 关联起来,需要使用 observe()方法。这个方法接收两个必需的参数:要观察其变化的 DOM 节点,以及一个 MutationObserverInit 对象。

        MutationObserverInit 对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。例如,下面的代码会创建一个观察者(observer)并配置它观察元素上的属性变化:

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

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

        注意,回调中的 console.log()是后执行的。这表明回调并非与实际的 DOM 变化同步执行。

        回调与 MutationRecord连续修改会生成多个 Muta

        每个回调都会收到一个 MutationRecord 实例的数组。MutationRecord 实例包含的信息包括发生了什么变化,以及 DOM 的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组

        连续修改会生成多个 MutationRecord 实例,下次回调执行时就会收到包含所有这些实例的数组,顺序为变化事件发生的顺序÷

        属 性说 明
        target被修改影响的目标节点type字符串,表示变化的类型:"attributes"、"characterData"或"childList"oldValue如果在 MutationObserverInit 对象中启用(
        attributeOldValue 或 characterData OldValue为 true),
        "attributes"或"characterData"的变化事件
        会设置这个属性为被替代的值"childList"类型的变化始终将这个属性设置为 null
        attributeName对于"attributes"类型的变化,这里保存被修改属性的名字其他变化事件会将这个属性设置为 nullattributeNamespace对于使用了命名空间的"attributes"类型的变化,
        这里保存被修改属性的名字其他变化事件会将这个属性设置为 null
        addedNodes对于"childList"类型的变化,返回包含变化中添加节点的 NodeList
        默认为空 NodeList
        removedNodes对于"childList"类型的变化,返回包含变化中删除节点的 NodeList
        默认为空 NodeList
        previousSibling对于"childList"类型的变化,返回变化节点的前一个同胞 Node
        默认为空 NodeLis
        nextSibling对于"childList"类型的变化,返回变化节点的前一个同胞 Node
        默认为空 NodeLis

        传给回调函数的第二个参数是观察变化的 MutationObserver 的实例,演示如下:

        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()方法。下面的例子演示了同步调用disconnect()之后,不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调:

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

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

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

        复用 MutationObserver

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

        验证方法就是disconnect()方法是一个“一刀切”的方案,调用它会停止观察所有目标:

        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 }); 
        observer.disconnect(); 
        // 修改两个子节点的属性
        childA.setAttribute('foo', 'bar'); 
        childB.setAttribute('foo', 'bar');
        // (没有日志输出)
        

        重用 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 与观察范围

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

        下表列出了 MutationObserverInit 对象的属性。

        属 性说 明
        subtree布尔值,表示除了目标节点,是否观察目标节点的子树(后代)
        如果是 false,则只观察目标节点的变化;如果是 true,则观察目标节点及其整个子树
        默认为 false
        attributes布尔值,表示是否观察目标节点的属性变化
        默认为 false
        attributeFilter字符串数组,表示要观察哪些属性的变化
        把这个值设置为 true 也会将 attributes 的值转换为 true
        默认为观察所有属性
        attributeOldValue布尔值,表示 MutationRecord 是否记录变化之前的属性值
        把这个值设置为 true 也会将 attributes 的值转换为 true
        默认为 false
        characterData布尔值,表示修改字符数据是否触发变化事件
        默认为 false
        characterDataOldValue布尔值,表示 MutationRecord 是否记录变化之前的字符数据
        把这个值设置为 true 也会将 characterData 的值转换为 true
        默认为 false
        childList布尔值,表示修改目标节点的子节点是否触发变化事件
        默认为 false

        注意 在调用 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', 'baz'); 
        document.body.setAttribute('foo', 'qux'); 
        // 每次变化都保留了上一次的值
        // [null, 'bar', 'baz']
        

        观察字符数据

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

        let observer = new MutationObserver( 
         (mutationRecords) => console.log(mutationRecords)); 
        // 创建要观察的文本节点
        document.body.firstChild.textContent = 'foo'; 
        observer.observe(document.body.firstChild, { characterData: true }); 
        // 赋值为相同的字符串
        document.body.firstChild.textContent = 'foo'; 
        // 赋值为新字符串
        document.body.firstChild.textContent = '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))); 
        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"]
        

        观察子节点

        MutationObserver 可以观察目标节点子节点的添加和移除。要观察子节点,需要在 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)); 
        // 创建两个初始子节点
        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", 
        // } 
        // ]
        

        观察子树

        默认情况下,MutationObserver 将观察的范围限定为一个元素及其子节点的变化。可以把观察的范围扩展到这个元素的子树(所有后代节点),这需要在 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实例中,然后添加到记录队列。这个队列对每个 MutationObserver 实例都是唯一的,是所有 DOM变化事件的有序列表。

        记录队列

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

        takeRecords()方法

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

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

        这在希望断开与观察目标的联系,但又希望处理由于调用 disconnect()而被抛弃的记录队列中的MutationRecord 实例时比较有用。

        性能、内存与垃圾回收

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

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

        MutationObserver 的引用

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

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

        MutationRecord 的引用

        记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用。如果变化是childList 类型,则会包含多个节点的引用。记录队列和回调处理的默认行为是耗尽这个队列,处理每个 MutationRecord,然后让它们超出作用域并被垃圾回收

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


    起源地下载网 » 红宝书之第十四章: DOM

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元