《JavaScript 高级程序设计》第十四章 DOM 学习记录

131 阅读13分钟

1、节点层级

  • documen节点是每个文档的根结点

  • 文档元素是文档最外层元素,每个文档值呢个有一个文档元素,在HTML中,始终是<html>元素

  • 元素节点表示HTML元素,属性节点表示属性,文档类型节点表示文档类型,注释节点表示住宿。

1、Node类型

  • 所以节点类型都继承Node类型,共享个相关的基本属性和方法。

  • 每个节点都有nodeType属性,表示该节点类型

    节点类型常量值+描述
    Node.ELEMENT_NODE1元素节点
    Node.ATTRIBUTE_NODE2属性节点
    Node.TEXT_NODE3文本节点
    Node.CDATA_SECTION_NODE4CDATA节点
    Node.ENTITY_REFERENCE_NODE5实体引用节点
    Node.ENTITY_NODE6实体节点
    Node.PROCESSING_INSTRUCTION_NODE7处理指令节点
    Node.COMMENT_NODE8注释节点
    Node.DOCUMENT_NODE9文档节点
    Node.DOCUMENT_TYPE_NODE10文档类型节点
    Node.DOCUMENT_FRAGMENT_NODE11文档片段节点
    Node.NOTATION_NODE12符号节点
    if(someNode.nodeType == Node.ELEMENT_NODE) {
    	// ...
    }
    

1、nodeName 与nodeValue

  • 保存有关节点的信息,属性值取决于节点类型。

    if(someNode.nodeType == Node.ELEMENT_NODE) {
      // someNode.nodeName 表示标签名
      // someNode.nodeValue 始终为null
    }
    
    • 对于元素 nodeName表示标签名, nodeValue始终为null

2、节点关系

  • childrenNodes属性,包含NodeList实例

    • NodeList是类数组,存储可以按位置存取的有序节点。
    • NodeList不是Array实例,但是可以用中括号访问,也有length属性
    • NodeList是一个对DOM结构的查询,DOM结构变化会自动反映出来
    let firstChild = someNodes.childrenNodes[0]
    let secondChild = someNodes.childrenNodes.item(1)
    let count = someNodes.childrenNodes.length
    
  • parentNode属性,指向DOM树中的父元素

  • childrenNodes中的每个节点都是同一列表中其他节点的同胞节点。

    • previousSibing 表示上一个兄弟
    • nextSibing表示下一个兄弟
  • 父节点的第一个子节点firstChild,最后一个子节点lastChild

  • hasChildNodes()方法检测是否有子节点,等同于someNode.childNodes.length >= 1

  • ownerDocument只想代表整个文档的文档节点的指针

3、操纵节点

  • appendChild()
    • 在childNodes列表末尾添加节点,返回新添加的节点。
    • 如果是文档中已存在的节点,则会从之前的位置转移到新位置
      • 如果传入第一个子节点,则会变成最后一个子节点
  • insertBefore()
    • 在childNodes列表特定位置添加节点,返回新添加的节点。
    • 两个参数:要插入的节点和参照节点
    • 如果参照节点为null,等同appendChild()
    • 调用后会变成参照节点的前一个同胞节点
  • replaceChild()
    • 替换节点,删除已知节点,返回要替换的节点
    • 两个参数:要插入的节点和要替换的节点
  • removeChild()
    • 移除节点,而非替换,返回被移除的节点
    • 参数:要移除的节点

4、其他方法

  • cloneNode() 返回与调用它的节点一模一样的节点。
    • 参数布尔值,表示是否深复制(复制节点及其整个子DOM树)
    • 不会复制添加到DOM节点的JavaScript属性,如事件处理程序。只复制HTML属性,以及可选的复制子节点。
  • normalize()处理文档子树中的文本节点,将空文本的文本节点或文本节点之间互为同胞关系删除或合并掉。

2、Document类型(9)

  • 文档节点的类型,是HTMLDocument的实例,表示整个HTML页面
    • nodeType: 9
    • nodeName: #document
    • nodeValue:null
    • parentNode:null
    • ownerDocument:null
    • 子节点可以是DocumentType(最多一个)、Element(最多一个)、ProcessingInstructoin或Comment类型

1、文档子节点

  • documentElement始终指向<html>元素
  • body直接指向<body>元素
  • doctype访问<!doctype>标签

2、文档信息

  • title包含<title>元素中的文本,显示在浏览器窗口或标题栏,可读可写
  • URL当前页面的完整URL
  • domain 页面的域名,
    • 可设置,但有限制,必须是原domain包含的值
    • 只能放松不能收紧
    • 修改成相同domain可以用来嵌套窗格通信
  • referrer 链接到当前页面的那个页面的URL

3、定位元素

  • getElementById()

    • 传入id,找到返回,没找到返回null
  • getElementByTagName()

    • 传入标签名,返回NodeList,在HTML文档中返回HTMLCollection对象

    • HTMLCollection

      • namedItem(),通过name属性取得某一项引用
      • 通过中括号获取
      <img src="xxx" name="myImage">
      // images.namedItem("myImage")
      // images["myImage"]
      
    • 取得所有元素传入*

  • getElementsByName()

    • 返回具有给定name属性的所有元素

4、特殊集合

  • document.anchors 包含文档所有带name属性的<a>元素
  • document.forms 包含文档所有<form>元素
  • document.images 包含文档所有<img>元素
  • document.links 包含文档所有带href属性的<a>元素

5、DOM兼容性检测

  • document.implementation

6、文档写入

  • write()、writeln()、open()、close()

3、Element类型

  • 表示XML或HTML元素,对外暴露出访问元素标签名、子节点和属性能力
    • nodeType:1
    • nodeName:元素的标签名
    • nodeValue:null
    • parentNode:Document或Element对象
  • 可以通过nodeName或tagName获取标签名(HTML全大写,XML实际大小)

1、HTML元素

  • 所有HTML元素都通过HTMLElement类型表示,包括其直接实例和间接实例
  • HTMLElement直接继承Element并增加了一些属性
    • id:元素在文档中的唯一标识符
    • title:包含元素的额外信息,通常以提示条形式展示
    • lang:元素内容的语言代码
    • dir:语言书写方向
    • className:相当于class属性,指定元素css类

2、取得属性

  • getAttribute()
    • 传入的属性名与实际的属性名要一致
    • 不存在返回null
    • 能获取非HTML语言正式定义的自定义属性的值。
    • 属性名不区分大小写
    • 自定义属性应该用data-前缀方便验证
    • 所有属性都可以通过相应的DOM元素对象的属性来取得。
    • DOM元素对象的属性包括直接映射的五个属性和所有的公认(非自定义)属性。
    • 通过DOM对象访问的属性与getAttribute()获得的属性区别
      • style属性
        • getAttribute()返回css字符串
        • DOM对象访问的属性是一个(CSSStyleDeclaration)对象
      • 事件(如onclick)
        • getAttribute()返回字符串形式的源代码
        • DOM对象访问的属性是一个JavaScript函数

3、设置属性

  • setAttribute()
    • 两个参数,要设置的属性名和属性值。
    • 如果属性名已存在,则会进行替换。
    • 适用于HTML属性,也适用自定义属性
    • 设置的属性名会规范为小写形式。
    • 也可以直接给DOM对象的属性赋值设置元素属性的值。
    • DOM对象上添加自定义属性,不会让他自动变成元素属性。
  • removeAttribute()
    • 删除属性的值,把整个属性完全从元素中去掉。

4、attributes属性

  • attributes属性包含一个NameedNodeMap实例,每个属性都表示为一个Attr节点。
    • getNamedItem(name) 返回nodeName属性等于name的节点
    • removeNameedItem(name) 删除nodeName属性等于name的节点
    • setNamedItem(node) 向列表中添加node节点,以其nodeName为索引
    • item(pos) 返回索引位置pos处的节点。
  • attributes属性中的每个节点的nodeName是对应属性的名字,nodeValue是属性的值。
  • 最有用的场景是需要迭代元素上所有属性的时候。

5、创建元素

  • document.createElement() 创建新元素
    • 接收一个参数,即要创建元素的标签名。
    • 创建之后可以为其添加属性
    • 创建之后需要使用添加方法添加到文档中。

6、元素后代

  • childNodes属性包含元素所有子节点,包括其他元素、文本节点、注释或处理指令。

  • 执行某个操作前可以先检测一下节点的nodeType(或者使用后面的children)

    for(let i = 0, len = element.childNodes.length; i < len; i++) {
      if(element.childNodes[i].nodeType === 1) {
        ...
      }
    }
    

4、Text类型

  • Text节点由Text类型表示,包含按字面解释的纯文本,也可以包含转义后的HTML字符串。
    • nodeType:3
    • nodeName:#text
    • nodeValue:节点保存的文本
    • parentNode:Element对象
    • 不支持子节点
  • Text节点中包含的文本可以通过nodeValue属性访问,也可以通过data属性访问。
    • appendData(text) 向节点末尾添加文本text
    • deleteDate(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的文本
    • length属性获取本本节点中包含的字符数量。等于nodeValue.length和data.length
  • 默认情况下每个元素最多只能有一个文本节点。
  • 修改文本节点的内容时,大于号,小于号或引号会被转义

1、创建文本节点

  • document.createTextNode()创建新文本节点
    • 接收一个参数,即要插入节点的文本。
    • 创建之后需要使用添加方法添加到文档中。

2、规范化文本节点

  • normalize()

3、拆分文本节点

  • splitText()

5、Comment类型

  • 注释通过Comment类表示
    • nodeType:8
    • nodeName:#comment
    • nodeValue:注释的内容
    • parentNode:Document或Element对象
    • 不支持子节点
  • Comment与Text类型继承同一个基类(CharacterData),拥有除了splitText()外的所有方法。
  • 注释的内容可通过nodeValue或data获取。
  • document.createComment()创建注释节点

6、CDATASection类型

  • XML特有,继承Text类型。
    • nodeType:4
    • nodeName:#cdata-section
    • nodeValue:CDATA区块内容
    • parentNode:Document或Element对象
    • 不支持子节点
  • document.createCDataSection()传入节点内容创建

7、DocumentType类型

  • 包含文档的文档类型(doctype)信息
    • nodeType:10
    • nodeName:文档类型的名称
    • nodeValue:null
    • parentNode:Document
    • 不支持子节点
  • 不支持动态创建,只能在解析文档代码时创建。
    • 保存在document.doctype
    • 包含三个属性:name、entities和notations
      • name表示文档类型的名称
      • entities 是这个文档类型描述的实体的NamedNodeMap 通常为空
      • natations 是这个文档类型描述的表示法的NamedNodeMap 通常为空

8、DocumentFragment类型

  • “轻量级”文档,能够包含和操作节点,却没有完整文档那样额外的消耗。
    • nodeType:11
    • nodeName:#document-fragment
    • nodeValue:null
    • parentNode:null
  • 不能直接将文档片段添加到文档,作用是充当其他要被添加到文档的节点的仓库。
  • document.createDocumentFragment()创建文档片段
  • 文档片段从Node类型继承了所有文档类型具备的可执行DOM操作的方法。如果文档中的一个节点被添加到文档片段,则该节点会文档树中移除,不再渲染。
  • 通过文档片段可以避免多次渲染,减少性能消耗。

9、Attr类型

  • 元素数据在DOM中通过Attr类型表示。是存在于元素attributes属性中的节点

    • nodeType:2
    • nodeName:属性名
    • nodeValue:属性值
    • parentNode:null
    • HTML中不支持子节点,XML中子节点可以是Text或EntityReference
  • 三个属性

    • name 属性名
    • value 属性值
    • specified 表示属性使用的是默认值还是被指定的值
  • document.createAttrubute()创建新的Attr节点

    let attr = document.createAttribute('align')
    attr.value = 'left'
    element.setAttributeNode(attr)
    
  • getAttributeNode()获取Attr节点

  • 以上方法都不建议使用。

2、DOM编程

1、动态脚本

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

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

    • 引入外部文件
    function loadScript(url) {
      let script = document.createElement('script')
    	script.src = url
    	document.body.appendChild(script)
    }
    
    • 直接插入源代码
    function loadScriptString(code) {
      let script = docuemnt.createElement('script')
      script.type = "text/javascript"
      try {
        // 兼容Safari3以下
        script.appendChild(document.createTextNode(code))
      }catch(ex) {
        // 兼容旧版IE
        script.text = code 
      }
      document.body.appendChild(script)
    }
    
  • 使用innerHtml创建的<script>元素永远不会执行

2、动态样式

  • <link>用于包含CSS外部文件

    function loadStyles(url) {
      let link = document.createElemt('link')
      link.rel = 'stylesheet'
      link.type = 'text/css'
      link.href = 'url'
      let head = document.getElementsByTagName('head')[0]
      head.appendChild(link)
    }
    
  • <style>用于添加嵌入样式

    function loadStylesString(css) {
      let style = documnet.createElement('style')
      style.tyle = 'text/css'
      try{
        style.appendChild(document.createTextNode(css))
      }catch(ex) {
        // 兼容IE 但容易崩溃
        style.stylesheet.cssText = css
      }
      let head = document.getElementsByTagName('head')[0]
      head.appendChild(style)
    }
    

3、操作表格

  • <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),在表元集合给定位置插入一个表元,返回该表元的引用

4、使用NodeList

  • 理解NodeList对象和NamedNodeMap、HTMLCollection
  • 这3个集合类型都是"实时的",文档结构的变化会实时的在它们身上反应出来。
  • 最好限制操作NodeList的次数,因为每次查询都会搜索整个文档,最好把查询到的NodeList缓存起来。

3、MutationObserver接口

  • 可以在DOM修改时异步执行回调
  • 可以用来观察整个文档、DOM树的一部分,或整个元素。
  • 可以观察元素属性、子节点、文本,或者前三者任意组合的变化

1、基本用法

  • 通过调用MutationObserver构造函数传入回调函数来创建

1、observe()方法

  • 与DOM进行关联,接收两个必需参数:

    • 要观察其变化的DOM节点
    • 一个MutationObserverInit 对象
  • MutationObserverInit 对象是控制观察哪方面的变化,是一个键值对形式配置选项的字典。

    let observer = new MutationObserver(()=>console.log('<body> attributes changed'))
    observer.observe(document.body, {attributes: true})
    
    • <body>元素任何属性的变化都会被MutationObserver实例发现
    • 会异步执行注册的回调函数

2、回调与MutationRecord

  • 每个回调都会收到一个MutationRecord实例的数组。包含的信息包括发生了什么变化,以及DOM的哪一部分受到了影响。

  • 可能有多个满足观察条件的事件,每次执行回调都会传入一个包含按顺序入队的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"
    //  }
    // ]
    document.body.className = 'foo'
    document.body.className = 'bar'
    document.body.className = 'baz'
    // [MutationRecord, MutationRecord, MutationRecord]
    
  • MutationRecord 实例属性

    属性说明
    target被修改影响的目标节点
    type变化类型,"attributes"、"characterData"或"childList"
    oldValue旧值,需要在MutationObserverInit 中启用
    attributeName对"attributes" 类型变化,保存被修改属性名字
    attributeNamespace对使用命名空间的"attributes" 类型变化,保存被修改属性名字
    addedNodes对应childList变化,包含变化中添加节点的NodeList
    removedNodes对应childList变化,包含变化中删除节点的NodeList
    previousSibling对应childList变化,返回变化节点的前一个同胞Node
    nextSibling对应childList变化,返回变化节点的后一个同胞Node
  • 传给回调函数的第二个实例是观察变化的MutationObserver实例。

3、disconnect() 方法

  • 提前终止执行回调,可以调用disconnect()方法
  • 同步调用disconnect()后,不仅会停止此后事件的回调,也会抛弃已经加入任务队列要异步执行的回调

4、复用MutationObserver

  • 多次调用observer()方法,可以复用一个MutationObserver对象观察多个不同的目标节点。
  • disconnect()方法是个"一刀切"的方案,会停止观察所有目标

5、重用MutationObserver

  • 调用disconnect()并不会结束MutationObserver 的生命。还可以重新使用这个观察者,再将它关联到新的目标节点。

2、MutatoinObserverInit 与 观察范围

  • MutatoinObserverInit 用来控制对目标节点的观察范围。
属性说明
subtree是否观察目标节点子树,默认false
attributes是否观察目标节点属性变化,默认false
attributeFilter字符串数组,表示观察哪些属性,默认所有
attributeOldValue是否记录变化之前的属性值,默认false
characterData是个观察字符数据修改,默认false
characterDataOldValue是否记录变化之前的字符数据,默认false
childList目标节点子节点是否触发变化事件,默认false

1、观察属性

  • MutationObserver可以观察节点属性的添加、移除和修改。
  • 需要在MutatoinObserverInit 设置attributes为true
  • 默认观察所有属性,可以通过attributeFilter 设置白名单
  • 保存原有属性,设置attributeOldValue

2、观察字符数据

  • MutationObserver可以观察文本节点(Text、Comment)中字符的添加、删除和修改。
  • 需要在MutatoinObserverInit 设置characterData为true
  • 保存原有字符数据,设置characterDataOldValue

3、观察子节点

  • MutationObserver可以目标节点子节点的添加和移除
  • 需要在MutatoinObserverInit 设置childList为true
  • 对子节点重新排序会报高两次事件变化

4、观察子树

  • 默认情况MutationObserver观察范围是一个元素及其子节点的变化。可以把范围扩展到这个元素的子树。
  • 需要在MutatoinObserverInit 设置subtree为true

3、异步回调与记录队列

  • 出于性能考虑而设计的,核心是异步回调与记录队列模型。每次变化会保存在MutationRecord实例中,然后添加到记录队列
  • 这个队列对每个MutationObserver实例都是唯一的,是所有DOM事件变化的有序列表。

1、记录队列

  • 仅当之前没有已排期的微任务回调时,才会将观察者注册的回调作为微任务调度到任务队列上,可以保证记录队列的内容不会被回调处理两次。
  • 回调执行后,记录队列会清空,内容会丢弃。

2、takeRecords() 方法

  • 调用MutationObserverd的takeRecords()方法可以清空记录队列,取出并返回其中所有MutationRecord实例。
  • 在希望断开与观察目标来的联系,又希望处理由于调用disconnect()而被抛弃的记录队列中的MutationRecord实例时使用。

4、性能、内存与垃圾回收

  • 相比之前的MutaionEvent有了很大进步,但仍然有代价。
1、MutationObserver的引用
  • MutationObserver实例与目标之前的引用关系时非对称的,是弱引用。不会妨碍垃圾回收程序回收程序回收目标节点。
  • 目标节点对MutationObserver是强引用,如果目标节点从DOM中移除,二后被垃圾回收,则关联的MutationObserver 也会被垃圾回收。

2、MutationRecord的引用

  • MutationRecord实例至少包含对已有DOM节点的一个引用,如果变化的是childList类型,则会包含多个节点的引用。
  • 如果要保存变化记录,保存MutationRecord实例,也就会保存其引用节点。因此会妨碍这些节点被回收。
  • 可以从每个MutationRecord中抽取最有用信息,保存到一个新对象里,然后抛弃MutationRecord,这样可以尽快地释放内存。