JavaScript学习笔记(12):DOM基础
DOM节点
任何HTML或XML文档都可以用DOM表示为一个由节点构成的层级结构。
节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系,这些关系构成了层级,让标记可以表示为一个以特定节点为根的树形结构。
节点
document节点表示每个文档的根节点,根节点的唯一子节点是文档元素。
文档元素是文档最外层的元素,所有其他元素都存在于这个元素之内。
每个文档只能有一个文档元素,在HTML页面中,文档元素始终是<html>
元素;在XML文档中,则没有这样预定义的元素,任何元素都可能成为文档元素。
HTML中的每段标记都可以表示为这个树形结构中的一个节点,DOM中总共有12种节点类型。
在JavaScript中,所有节点类型都继承Node类型,因此所有类型都共享相同的基本属性和方法,以下即为Node类型的共有的属性。如果没有强调,下列属性都是只读的。
nodeType属性
每个节点都有nodeType属性,表示该节点的类型。节点类型由定义在Node类型上的12个数值常量表示:
Node.ELEMENT_NODE
(1):元素节点。Node.ATTRIBUTE_NODE
(2):元素的耦合属性。已弃用。Node.TEXT_NODE
(3):元素或者属性中的文字。Node.CDATA_SECTION_NODE
(4):CDATASection。Node.ENTITY_REFERENCE_NODE
(5):XML 实体引用节点。已弃用。Node.ENTITY_NODE
(6):XML<!ENTITY ...>
节点。已弃用。Node.PROCESSING_INSTRUCTION_NODE
(7):用于XML文档的ProcessingInstruction。Node.COMMENT_NODE
(8):注释节点。Node.DOCUMENT_NODE
(9):Document节点。Node.DOCUMENT_TYPE_NODE
(10):描述文档类型的DocumentType节点。Node.DOCUMENT_FRAGMENT_NODE
(11):DocumentFragment节点。Node.NOTATION_NODE
(12):XML<!NOTATION ...>
节点。已弃用。
节点类型可通过与这些常量比较来确定,比如:
1 |
|
比较了someNode.nodeType
与Node.ELEMENT_NODE
,如果两者相等,则意味着someNode是一个元素节点。
浏览器并不一定支持所有节点类型,开发者最常用到的是元素节点和文本节点,因此我们着重强调元素节点和文本节点所拥有的属性和方法,并非所有节点都有这些属性和方法。
nodeName属性
返回一个存储了当前节点的节点名称的字符串。
对于元素节点来说,该属性为标签名,且在XML中为区分大小写的标签名,在HTML中为全大写。
nodeValue属性
nodeValue是可写的,通过它可以改变其值。
文本节点、注释节点和CDATA节点来说的nodeValue为该节点的文本内容。
属性节点的nodeValue为该属性的属性值。
ProcessingInstruction节点的nodeValue为整个标签的文本内容。
其余节点的nodeValue都为null。
childNodes属性
节点关系即文档树中节点之间的关系,文档中所有节点都与其他节点有关系。
每个节点都有一个childNodes属性,其中包含一个NodeList的实例。NodeList是一个类数组对象,用于存储可以按位置存取的有序节点。
但是注意!NodeList对象只在此处为动态的,即其内容会随着页面DOM结构的变化实时更新,但在其他情况下(如document.querySelectorAll()
方法返回的),NodeList对象都是静态的,只是一个当时的快照,不会随着文档DOM结构变化而更新。
parentNode属性
每个节点都有一个parentNode属性,指向其DOM树中的父元素。childNodes中的所有节点都有同一个父元素,因此它们的parentNode属性都指向同一个节点。
previousSibling属性和nextSibling属性
childNodes列表中的每个节点都是同一列表中其他节点的同胞节点,使用previousSibling和nextSibling可以在这个列表的节点间导航。
这个列表中第一个节点的previousSibling属性是null,最后一个节点的nextSibling属性也是null。
firstChild属性和lastChild属性
firstChild和lastChild分别指向节点的childNodes中的第一个和最后一个子节点。
ownerDocument属性
ownerDocument属性是每个节点都有的,指向代表整个文档的文档节点的指针。
hasChildNodes方法
返回元素是否有孩子,hasChildNodes()
方法如果返回true则说明节点有子节点。相比查询childNodes的length属性,这个方法更方便。
appendChild方法
appendChild()
方法用于在childNodes列表末尾添加节点,同时更新所有有关的节点关系。
insertBefore方法
insertBefore()
方法接收两个参数:要插入的节点和参照节点。这样可以把节点插入到childNodes列表中指定子节点前面。如果它第二个参数为null,那么它和appendChild()
方法效果一样。
replaceChild方法
replaceChild()
方法接收两个参数:新的节点和要被替换的节点。这样可以用新节点替换掉childNodes列表中的指定节点。要替换的节点会被返回并从文档树中完全移除。
removeChild方法
removeChild()
方法接收一个参数,即要移除的节点,返回值是被移除的节点。这样可以从节点的孩子中删除掉一个节点。
cloneNode方法
cloneNode()
方法会返回与调用它的节点一模一样的节点。
cloneNode()
方法接收一个布尔值参数,表示是否深复制。在传入true参数时,会进行深复制,即复制节点及其整个子DOM树。如果传入false,则只会复制调用该方法的节点。
复制返回的节点属于文档所有,但尚未指定父节点,所以可称为孤儿节点。之后可将其插入文档结构中。
normalize方法
在节点上调用normalize()
方法会检测这个节点的所有后代,如果发现空文本节点,则将其删除;同时将多个连续的文本节点合并为一个文本节点。
Document类型
在浏览器中,文档对象document是HTMLDocument的实例,表示整个HTML页,而HTMLDocument又继承自Document面。
Document类型的节点有以下特征:
- nodeType值为9。
- nodeName值为
"#document"
。 - nodeValue值为null。
- parentNode值为null。
- ownerDocument值为null。
- 子节点可以是DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction或Comment类型。
理论上讲,出现在文档中的内容都应该是document的子节点,但是出现在<html>
元素之外的注释可能被忽略或部分忽略。
documentElement属性
document.childNodes
中始终有<html>
元素,但使用document.documentElement
属性可以更快更直接地访问该元素,该属性始终指向HTML页面中的<html>
元素
body属性
document对象还有一个body属性,直接指向<body>
元素,因为这个元素是开发者使用最多的元素。
doctype属性
DocumentType也是Document类型可能的子节点。
<! doctype>
标签是文档中独立的部分,其可以通过document.doctype
来访问。
title属性
通过这个属性可以读写页面的标题,修改后的标题也会反映在浏览器标题栏上。
但注意这个属性和<title>
元素是无关的。
链接属性
document对象包含三个与链接有关的属性,分别是URL属性、domain属性和referrer属性。
- URL属性:包含当前页面的完整URL(地址栏中的URL)。
- domain属性:包含页面的域名。
- referrer属性:包含链接到当前页面的那个页面的URL。如果当前页面没有来源,则referrer属性包含空字符串。
所有这些信息都可以在请求的HTTP头部信息中获取,只是在JavaScript中通过这几个属性暴露出来而已。
例如:document.URL
是http://www.wrox.com/WileyCDA/
,则document.domain
就是www.wrox.com
。
在这些属性中,只有domain属性是可以设置的。出于安全考虑,给domain属性设置的值是有限制的,不能给这个属性设置与当前域名毫不相干的值。因此这个属性的用处也非常有限,也仅仅是能够在个别场景下实现跨域访问。
获取元素的方法
document对象上的以下方法可以用来获取页面上的元素:
getElementById()
方法:根据元素ID获取元素DOM。只返回一个元素,没有找到返回null。多个元素ID相同则返回第一个。getElementsByTagName()
方法:根据标签名获取元素。返回一个HTMLCollection对象。可接受通配符"*"
作为参数,这将会获取所有标签。虽然规范要求其区分大小写,但实际上在HTML中它是不区分大小写的,在XML中区分大小写。getElementsByName()
方法:根据name属性获取元素。返回一个HTMLCollection对象。getElementsByClassName()
方法:根据类名获取元素。返回一个HTMLCollection对象。(HTML5扩展方法)
这些方法中返回的是HTMLCollection对象,它和NodeList对象很像,区别是HTMLCollection对象始终是动态的,是一个实时的列表。
HTMLCollection对象还有两个额外的方法:
item()
方法:可通过下标来访问HTMLCollection中的元素。namedItem()
方法:可通过标签的name属性取得HTMLCollection对象中某一项的引用。
而同时,还可以通过中括号的方式访问其中的项目,如果中括号中的是字符串,那么像访问字典一样地也可以根据name访问到某一项,就像调用了namedItem()
方法一样;如果提供的是索引,那么就像访问数组一样地访问其中的元素,就像调用了item()
方法一样。
因此HTMLCollection对象既像数组又像字典。
特殊集合
document对象上还暴露了几个特殊集合,这些集合也都是HTMLCollection的实例,它们是访问文档中公共部分的快捷方式:
document.anchors
:包含文档中所有带name属性的<a>
元素。document.applets
:包含文档中所有<applet>
元素(<applet>
元素已经不建议使用,因此这个集合已经废弃)。document.forms
:包含文档中所有<form>
元素。document.images
:包含文档中所有<img>
元素。document.links
:包含文档中所有带href属性的<a>
元素。
DOM兼容性检测
由于DOM有多个Level和多个部分,因此确定浏览器实现了DOM的哪些部分是很必要的。
document.implementation
属性是一个对象,其中提供了与浏览器DOM实现相关的信息和能力。这个对象上只有一个方法,即hasFeature()
。这个方法接收两个参数:特性名称和DOM版本。如果浏览器支持指定的特性和版本,则hasFeature()
方法返回true。
但要注意:由于实现不一致,因此hasFeature()
的返回值并不可靠。目前这个方法已经被废弃,不再建议使用。为了向后兼容,目前主流浏览器仍然支持这个方法,但无论检测什么都一律返回true。
文档写入
document对象有一个古老的能力,即向网页输出流中写入内容。这个能力对应4个方法:write()
、writeln()
、open()
和close()
。
其中,write()
和writeln()
方法都接收一个字符串参数,可以将这个字符串写入网页中。write()
简单地写入文本,而writeln()
还会在字符串末尾追加一个换行符(\n
)。
在页面渲染期间通过document.write()
可以向文档中输出内容,如果在页面加载完之后再调用document.write()
,则输出的内容会重写整个页面。
open()
和close()
方法分别用于打开和关闭网页输出流。在调用write()
和writeln()
时,这两个方法都不是必需的。
严格的XHTML文档不支持文档写入。因此对于内容类型为application/xml+xhtml
的页面,这些方法不起作用。
Element类型
Element表示XML或HTML元素,对外暴露出访问元素标签名、子节点和属性的能力。Element类型的节点具有以下特征:
- nodeType的值等于1。
- nodeName值为元素的标签名。
- nodeValue值为null。
- parentNode值为Document或Element对象。
- 子节点可以是Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference类型。
可以通过nodeName或tagName属性来获取元素的标签名(在HTML中全大写)。
如果不确定脚本是在HTML文档还是XML文档中运行,最好将标签名转换为小写形式,以便于比较。
HTML元素
HTML元素都通过HTMLElement类型表示,HTMLElement直接继承Element并增加了一些属性,它们是所有HTML元素上都有的标准属性:
- id:元素在文档中的唯一标识符。
- title:包含元素的额外信息,通常以提示条形式展示。
- lang:元素内容的语言代码(很少用)。
- dir:语言的书写方向(
"ltr"
表示从左到右,"rtl"
表示从右到左,同样很少用)。 - className:相当于class属性,用于指定元素的CSS类(因为class是ECMAScript关键字,所以不能直接用这个名字)。
所有这些都可以用来获取对应的属性值,也可以用来修改相应的值。
获取元素属性
与属性相关的DOM方法主要有3个:
getAttribute()
方法:获取属性的值指定属性不存在则返回null。setAttribute()
方法:设置属性值。参数为属性名和要设置的值。通过该方法设置的属性名都会变为规范的小写形式。removeAttribute()
方法:移除属性。
注意:属性名不区分大小写。且根据HTML5规范的要求,自定义属性名应该前缀data-
以方便验证。
属性可以通过getAttribute()
方法访问,也可通过DOM对象访问,大多情况下两种方式获得的值是一样的,只有两个属性例外:
- style属性:在使用
getAttribute()
访问style属性时返回的是CSS字符串;在通过DOM对象的属性访问时,style属性返回的是一个CSSStyleDeclaration对象。 - 事件处理程序(或者事件属性):如果使用
getAttribute()
访问事件属性,则返回的是字符串形式的源代码。而通过DOM对象的属性访问事件属性时返回的则是一个JavaScript函数(未指定该属性则返回null)。
同样地,设置属性也可以通过getAttribute()
方法或DOM对象。但是直接在DOM对象上添加的自定义属性并不会自动成为标签的属性,通过getAttribute()
方法是获取不到的。
attributes属性
Element类型是唯一使用attributes属性的DOM节点类型。
Element对象的attributes属性包含一个NamedNodeMap实例,也是一个“实时”的集合。元素的每个属性都表示为一个Attr节点,并保存在这个NamedNodeMap对象中。
NamedNodeMap对象包含下列方法:
getNamedItem(name)
:返回nodeName属性等于name的节点;removeNamedItem(name)
:删除nodeName属性等于name的节点;setNamedItem(node)
:向列表中添加node节点,以其nodeName为索引;item(pos)
:返回索引位置pos处的节点。
attributes属性中的每个节点的nodeName是对应属性的名字,nodeValue是属性的值。每个节点都是nodeType的值为2的Node节点。
attributes属性最有用的场景是需要迭代元素上所有属性的时候。
创建元素
使用document.createElement()
方法创建新元素。这个方法接收一个参数,即要创建元素的标签名。
使用createElement()
方法创建新元素的同时也会将其ownerDocument属性设置为document。此时,可以再为其添加属性、添加更多子元素。
要把创建的元素添加到文档树,可以使用appendChild()
、insertBefore()
或replaceChild()
等方法。
某些元素可以使用new关键字来创建,如Image、Option等。
Text类型
Text节点即文档中的文本节点,有以下特征:
- nodeType的值等于3。
- nodeName值为”#text”。
- nodeValue值为节点中包含的文本。
- parentNode值为Element对象。
- 不支持子节点。
属性
其特殊的属性主要也就是data属性,它和nodeValue属性是一样的,都表示文本的值,修改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的文本。
要想创建新的文本节点,可以使用document.createTextNode()
方法。
前文介绍了normalize()
方法,而splitText()
方法则是与其相反的方法。
Comment类型
注释节点的类型,该类型有以下特征:
- nodeType的值等于8。
- nodeName值为
"#comment"
。 - nodeValue值为注释的内容。
- parentNode值为Document或Element对象。
- 不支持子节点。
与Text类型相似,注释的实际内容可以通过nodeValue或data属性获得。
同样可以使用document.createComment()
方法创建注释节点,参数为注释内容字符串。
CDATASection类型
CDATASection类型表示XML中特有的CDATA区块。CDATASection类型继承Text类型,因此拥有所有Text节点的方法。CDATASection类型的节点具有以下特征:
- nodeType的值等于4。
- nodeName值为
"#cdata-section"
。 - nodeValue值为CDATA区块的内容。
- parentNode值为Document或Element对象。
- 不支持子节点。
CDATA区块只在XML文档中有效,因此某些浏览器比较陈旧的版本会错误地将CDATA区块解析为Comment或Element。
在XML文档中,可以使用document.createCDataSection()
并传入节点内容来创建CDATA区块。
DocumentType类型
DocumentType类型的节点包含文档的文档类型(doctype)信息,具有以下特征:
- nodeType的值等于10。
- nodeName值为文档类型的名称。
- nodeValue值为null。
- parentNode值为Document对象。
- 不支持子节点。
DocumentType对象在DOM Level 1中不支持动态创建,只能在解析文档代码时创建。
对于支持这个类型的浏览器,DocumentType对象保存在document.doctype
属性中。
DOM Level 1规定了DocumentType对象的3个属性:
- name:文档类型的名称。
- entities:这个文档类型描述的实体的NamedNodeMap。
- notations:这个文档类型描述的表示法的NamedNodeMap。
因为浏览器中的文档通常是HTML或XHTML文档类型,所以entities和notations列表为空。无论如何都只有name属性是有用的。
name属性包含文档类型的名称,即紧跟在<! DOCTYPE
后面的那串文本。比如下面的HTML 4.01严格文档类型:
1 |
|
对于这个文档类型,name属性的值是"html"
:
1 |
|
DocumentFragment类型
在所有节点类型中,DocumentFragment类型是唯一一个在标记中没有对应表示的类型。DOM将文档片段定义为“轻量级”文档,能够包含和操作节点,却没有完整文档那样额外的消耗。DocumentFragment节点具有以下特征:
- nodeType的值等于11。
- nodeName值为
"#document-fragment"
。 - nodeValue值为null。
- parentNode值为null。
- 子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection或EntityReference。
不能直接把文档片段添加到文档。相反,文档片段的作用是充当其他要被添加到文档的节点的仓库。
可以使用document.createDocumentFragment()
方法创建文档片段。
Attr类型
Attr类型的节点是存在于元素attributes属性中的节点。Attr节点具有以下特征:
- nodeType的值等于2。
- nodeName值为属性名。
- nodeValue值为属性值。
- parentNode值为null。
- 在HTML中不支持子节点。
- 在XML中子节点可以是Text或EntityReference。
属性节点尽管是节点,却不被认为是DOM文档树的一部分。Attr节点很少直接被引用,通常开发者更喜欢使用getAttribute()
、removeAttribute()
和setAttribute()
方法操作属性。
Attr对象上有3个属性:
- name:包含属性名(与nodeName一样)。
- value:包含属性值(与nodeValue一样)。
- specified:一个布尔值,表示属性使用的是默认值还是被指定的值。
可以使用document.createAttribute()
方法创建新的Attr节点,参数为属性名。可以使用元素的setAttributeNode()
方法将这个属性添加到元素上。
DOM编程
除了操作常规的页面元素之外,还有一些特殊的页面内容可以被操作。
动态脚本
使用JS可以动态地向页面上添加脚本,实现的方式为向页面添加script标签。
插入外部JS文件
我们通常这样在页面上引用JS文件:
1 |
|
那么我们就可以通过创建script标签——设置src属性——添加到页面这样的方式来引入JS文件:
1 |
|
注意:在把<script>
元素添加到页面之前,是不会开始下载外部文件的。
当然也可以把它添加到<head>
元素,同样可以实现动态脚本加载。
这个过程可以抽象为一个函数,比如:
1 |
|
插入内联JS代码
即插入像这样的script标签:
1 |
|
那么有两种方法。
方法一:创建script标签,为其添加一个Text子节点,Text内容为JS代码字符串。即:
1 |
|
方法二:创建script标签,为其设置text属性来添加JS代码字符串。即:
1 |
|
由于兼容性问题,旧版本的IE中不支持方法一中的访问script标签子节点,而Safari 3之前的版本不能正确支持方法二中的text属性,因此综合起来考虑兼容性,有以下方案:
1 |
|
以这种方式加载的代码会在全局作用域中执行,并在调用返回后立即生效。基本上,这就相当于在全局作用域中把源代码传给eval()
方法。
注意:通过innerHTML属性创建的<script>
元素永远不会执行。这样浏览器仍然会创建<script>
元素和其中的内容,但解析器会给这个<script>
元素打上永不执行的标签。
动态样式
CSS样式在HTML页面中可以通过两个元素加载:<link>
元素用于包含CSS外部文件,而<style>
元素用于添加嵌入样式。动态样式也是通过这两个元素像加载动态脚本一样加载的,
通过<link>
元素加载外部的样式表和加载外部的动态脚本一样很容易,而使用<style>
元素加载嵌入的样式会出现和加载嵌入的脚本一样的问题:低版本的IE不允许访问<style>
元素的内部节点,因此需要使用cssText属性代替。
但对于IE要小心使用styleSheet.cssText
:如果重用同一个<style>
元素并设置该属性超过一次,则可能导致浏览器崩溃。同样,将cssText设置为空字符串也可能导致浏览器崩溃。
动态添加的样式会立即生效,所有变化都会立即反映出来。
操作表格
表格是HTML中最复杂的结构之一。通过DOM编程创建<table>
元素,通常要涉及大量标签,包括表行、表元、表题等。
为了方便操作表格,HTML DOM给<table>
、<tbody>
和<tr>
元素添加了一些属性和方法,这些属性和方法极大地减少了创建表格所需的代码量。
table元素
<table>
元素上添加了以下属性和方法:
caption
:指向<caption>
元素(如果存在)。tBodies
:包含<tbody>
元素的HTMLCollection。tFoot
:指向<tfoot>
元素(如果存在)。tHead
:指向<thead>
元素(如果存在)。rows
:包含表示所有行的HTMLCollection。createTHead()
:创建<thead>
元素,放到表格中,返回引用。createTFoot()
:创建<tfoot>
元素,放到表格中,返回引用。createCaption()
:创建<caption>
元素,放到表格中,返回引用。deleteTHead()
:删除<thead>
元素。deleteTFoot()
:删除<tfoot>
元素。deleteCaption()
:删除<caption>
元素。deleteRow(pos)
:删除给定位置的行。insertRow(pos)
:在行集合中给定位置插入一行。
tbody元素
<tbody>
元素上添加了以下属性和方法:
rows
:包含<tbody>
元素中所有行的HTMLCollection。deleteRow(pos)
:删除给定位置的行。insertRow(pos)
:在行集合中给定位置插入一行,返回该行的引用。
tr元素
<tr>
元素上添加了以下属性和方法:
cells
:包含<tr>
元素所有表元的HTMLCollection。deleteCell(pos)
:删除给定位置的表元。insertCell(pos)
:在表元集合给定位置插入一个表元,返回该表元的引用。
NodeList对象
NodeList对象和其相关的NamedNodeMap、HTMLCollection对象在DOM编程中具有重要地位。
虽然设计者的初衷是使得这三种对象都是实时的,在规范中也说明了它们是实时的,但是由于性能的原因,浏览器在实现它们的时候,并不一定将其实现为实时的集合。
大多浏览器仅将Node.childNodes
返回的是NodeList对象设计为实时的,而其他情况下都是静态的,如document.querySelectorAll()
方法就会返回一个静态NodeList。
而NamedNodeMap对象和HTMLCollection对象总是实时的
。
由于历史原因,在DOM4之前,实现该接口的集合只能包含HTML元素,因此该接口才被称为HTMLCollection。
要注意动态的DOM集合很容易在遍历时陷入死循环,比如当你每次遍历都在往其中加入内容时,导致列表中的DOM元素反而越遍历越多,永远也遍历不完了。因此在遍历动态集合时,最好采用计数变量的循环方式,使得计数变量与集合的length属性作比较来决定循环的进行,当然更多时候最合适的做法是拷贝一份实时集合的静态副本,在静态副本上进行遍历。
MutationObserver接口
MutationObserver接口可以在DOM被修改时异步执行回调,使用MutationObserver可以观察整个文档的各个部分的变化。
基本用法
创建实例
MutationObserver的实例要通过调用MutationObserver构造函数并传入一个回调函数来创建:
1 |
|
observe方法
新创建的MutationObserver实例不会关联DOM的任何部分,要把这个observer与DOM关联起来,需要使用observe()
方法。
这个方法接收两个必需的参数:要观察其变化的DOM节点,以及一个MutationObserverInit对象。
MutationObserverInit对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。
例如,下面的代码会创建一个观察者(observer)并配置它观察<body>
元素上的属性变化:
1 |
|
执行以上代码后,<body>
元素上任何属性发生变化都会被这个MutationObserver实例发现,然后就会异步执行注册的回调函数,而<body>
元素后代的修改或其他非属性修改都不会触发回调进入任务队列。
这里的回调函数是异步的,并不会阻塞DOM变化。
MutationRecord
回调函数会收到一个MutationRecord实例的数组(队列)作为参数,此外传给回调函数的第二个参数是观察变化的MutationObserver的实例。
MutationRecord实例包含的信息包括DOM的哪一部分发生了什么变化。因为回调是异步执行的,执行之前可能同时发生多个满足观察条件的事件,传入的参数是一个包含按顺序入队的MutationRecord实例的数组。
MutationRecord实例具有的属性如下:
disconnect方法
默认情况下,只要被观察的元素不被垃圾回收,MutationObserver的回调就会响应DOM变化事件,要提前终止执行回调,可以调用disconnect()
方法:
1 |
|
复用和重用
多次调用observe()
方法,可以使用同一个MutationObserver对象观察多个不同的目标节点,此时,可以通过MutationRecord的target属性来区分目标节点。
而disconnect()
方法是一个“一刀切”的方案,它会使观察者停止观察所有目标。
但disconnect()
并不会结束MutationObserver的生命,我们还可以重新使用这个观察者,再调用observe()
方法将它关联到新的目标节点。
观察范围
MutationObserverInit对象可以控制对目标节点的观察范围(观察目标的哪些变化)。
观察者可以观察的事件包括属性变化、文本变化和子节点变化。
以下是可以通过MutationObserverInit对象进行的配置:
记录队列
MutationObserver接口出于性能考虑以异步回调与记录队列模型为核心。
为了在大量变化事件发生时不影响性能,每次观察到的变化信息会保存在MutationRecord实例中,然后添加到记录队列。这个队列对每个MutationObserver实例都是唯一的,是所有监听的DOM变化事件的有序列表。
MutationObserver的回调函数会进入微任务队列,并且是在队列中没有相同的任务时才会进入队列,防止回调函数重复执行。因此在回调函数中要负责处理队列中的每一个MutationRecord实例,否则函数结束后就认为这些记录变化的MutationRecord实例已经被处理过了,因此会清空它们导致内容丢失。
调用MutationObserver实例的takeRecords()
方法可以清空记录队列,取出并返回其中的所有MutationRecord实例。这在希望断开与观察目标的联系,但又希望处理由于调用disconnect()
而被抛弃的记录队列中的MutationRecord实例时非常有用。
内存与性能
MutationObserver实例与目标节点之间的引用关系是非对称的,MutationObserver仅拥有目标节点的弱引用,不会妨碍垃圾回收程序回收目标节点;而目标节点却拥有对MutationObserver的强引用。
如果所有的目标节点从DOM中被移除,随后被垃圾回收,关联的MutationObserver又没有其他引用的话,该MutationObserver对象会被垃圾回收。
每个MutationRecord实例都会包含对已有一个或多个DOM节点的引用,记录队列和回调处理的默认行为是耗尽这个队列,处理每个MutationRecord,然后让它们超出作用域并被垃圾回收。
如果保存这些MutationRecord实例,会妨碍它引用的节点被回收。因此最好从每个MutationRecord中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃MutationRecord。