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
2
3
if (someNode.nodeType == Node.ELEMENT_NODE) {
alert("Node is an element.");
}

比较了someNode.nodeTypeNode.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.URLhttp://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
2
<! DOCTYPE HTML PUBLIC "-// W3C// DTD HTML 4.01// EN"
"http:// www.w3.org/TR/html4/strict.dtd">

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

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

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="foo.js"></script>

那么我们就可以通过创建script标签——设置src属性——添加到页面这样的方式来引入JS文件:

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

注意:在把<script>元素添加到页面之前,是不会开始下载外部文件的。

当然也可以把它添加到<head>元素,同样可以实现动态脚本加载。

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

1
2
3
4
5
function loadScript(url) {
let script = document.createElement("script");
script.src = url;
document.body.appendChild(script);
}

插入内联JS代码

即插入像这样的script标签:

1
2
3
4
5
<script>
function sayHi() {
alert("hi");
}
</script>

那么有两种方法。

方法一:创建script标签,为其添加一个Text子节点,Text内容为JS代码字符串。即:

1
2
3
let script = document.createElement("script");
script.appendChild(document.createTextNode("function sayHi(){alert('hi'); }"));
document.body.appendChild(script);

方法二:创建script标签,为其设置text属性来添加JS代码字符串。即:

1
2
3
var script = document.createElement("script");
script.text = "functionsayHi(){alert('hi');}";
document.body.appendChild(script);

由于兼容性问题,旧版本的IE中不支持方法一中的访问script标签子节点,而Safari 3之前的版本不能正确支持方法二中的text属性,因此综合起来考虑兼容性,有以下方案:

1
2
3
4
5
6
7
8
9
10
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);
}

以这种方式加载的代码会在全局作用域中执行,并在调用返回后立即生效。基本上,这就相当于在全局作用域中把源代码传给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
let observer = new MutationObserver(() => console.log('DOM was mutated! '));

observe方法

新创建的MutationObserver实例不会关联DOM的任何部分,要把这个observer与DOM关联起来,需要使用observe()方法。

这个方法接收两个必需的参数:要观察其变化的DOM节点,以及一个MutationObserverInit对象。

MutationObserverInit对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。

例如,下面的代码会创建一个观察者(observer)并配置它观察<body>元素上的属性变化:

1
2
3
4
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, {
attributes: true
});

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

这里的回调函数是异步的,并不会阻塞DOM变化

MutationRecord

回调函数会收到一个MutationRecord实例的数组(队列)作为参数,此外传给回调函数的第二个参数是观察变化的MutationObserver的实例。

MutationRecord实例包含的信息包括DOM的哪一部分发生了什么变化。因为回调是异步执行的,执行之前可能同时发生多个满足观察条件的事件,传入的参数是一个包含按顺序入队的MutationRecord实例的数组

MutationRecord实例具有的属性如下:

disconnect方法

默认情况下,只要被观察的元素不被垃圾回收,MutationObserver的回调就会响应DOM变化事件,要提前终止执行回调,可以调用disconnect()方法:

1
2
3
4
5
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, {
attributes: true
});
observer.disconnect();

复用和重用

多次调用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




* 你好,我是大森。如果文章内容帮到了你,你可通过下方付款二维码支持作者 *