# 14. DOM
DOM(文档对象模型 Document Object Model),是 HTML 和 XML 文档的编程接口,DOM 描绘了一个层次化的结点树,允许开发人员添加、移除和修改页面的各个部分。1998年10月,DOM Level 1 成为 W3C 的推荐标准,为基本的文档结构及查询提供了接口。
# 节点层次
DOM 可以将 HTML 或 XML 文档描绘成一个由多层结点构成的结构,节点分为不同的类型,每种类型分别表示文档中不同的信息或标记,每个节点都拥有各自的特性、数据和方法,与其他节点存在某种关系。节点之间的关系,构成了层次,形成树形结构。
document 节点是每个文档的根节点,根节点的唯一子节点是 html
元素,称之为文档元素(documentElement),它是文档的最外层元素。文档中的其他所有元素都包含在其中。每个文档只能有一个文档元素,在 HTML 页面中文档元素始终是 <html>
元素。在 XML中,没有预定义的元素,任何元素都可能成为文档元素
# 节点(Node)类型
DOM Level 1 定义了名为 Node 的接口,该接口是所有 DOM 结点类型都必须实现的,Node 接口在 JS 中被实现为 Node 类型。除 IE 外所有浏览器都可以直接访问 Node,所有结点类型都继承自Node类型,因此所有的结点类型都共享相同的属性和方法, 每个节点都有一个 nodeType 属性,用于表明节点的类型,节点类型总共有 12 种,分别对应一个常量
Node.ELEMENT_NODE(1)
元素节点,最常见的一种,比如 span、body、h2 等都是元素节点Node.ATTRIBUTE_NODE(2)
特性(attribute)节点,element.attributes[0],一般元素节点上的属性(特性) 对象就是特性节点。比如 id、class、name 等。Node.TEXT_NODE(3)
文本节点,文字基本都是文本节点Node.CDATA_SECTION_NODE(4)
(只针对XML文档)Node.ENTITY_REFERENCE_NODE(5)
Node.ENTITY_NODE(6)
Node.PROCESSING_INSTRUCTION_NODE(7)
Node.COMMENT_NODE(8)
注释节点,注释Node.DOCUMENT_NODE(9)
document,文档节点。一个 html 只有一个, .title, .URL, .referrerNode.DOCUMENT_TYPE_NODE(10)
doctype节点,HTML5最顶部Node.DOCUMENT_FRAGMENT_NODE(11)
文档片段节点,属于中间节点,过度用Node.NOTATION_NODE(12)
// 判断节点类型
if (someNode.nodeType === Node.ELEMENT_NODE) { // 在IE中无效
alert("Node is an element.");
}
// IE 没有公开 Node 类型的构造函数,所以上面的方法在 IE 中不支持
if (someNode.nodeType === 1) { // 适用于所有浏览器
alert("Node is an element.");
}
浏览器并不支持所有的节点类型,开发者最常用的是元素节点和文本节点
# nodeName 与 nodeValue 属性
if (someNode.nodeType === 1) {
value = someNode.nodeName;
// 对于元素节点
// nodeName 始终为元素的标签名(大写),nodeValue 始终为 null
}
document.title = 'test' // 可以修改标题
document.nodeName // "#document"
document.nodeType // 9 Node.DOCUMENT_NODE
document.head.nodeName // "HEAD"
document.head.nodeType // 1 ELEMENT_NODE
document.node.nodeValue // null
# 节点关系
每个节点都有一个 childNodes 属性,它的值是一个 NodeList 对象,属于类数组对象。可以使用数组下表、item()、forEach、length 访问或遍历 NodeList 对象的内容。NodeList 对象是对 DOM 结果的查询,DOM 结构的变化会自动在 NodeList 对象中反映出来,它是实时的活动对象,而不是第一次获得内容的快照。
document.childNodes // NodeList(2) [html, html] 文档类型节点,文档节点
document.childNodes instanceof Array // false
// 1.子节点 childNodes 是 NodeList 对象
let firstChild = someNode.childNodes[0];
let secondChild = someNode.childNodes.item(1);
let count = someNode.childNodes.length;
可以通过数组的 slice 、Array.from 方法将类数组的 NodeList 对象,转为真正的数组
let arr = Array.prototype.slice.call(document.childNodes, 0)
arr instanceof Array // true
// 也可以使用 Array.from
let arr = Array.from(document.childNodes)
每一个元素节点都拥有下面的属性或方法:
childNodes
元素的子节点(NodeList 对象)parentNode
元素的父节点previousSibling
元素的前一个兄弟节点nextSibling
元素的后一个兄弟节点firstChild
元素的第一个子节点lastChild
元素的最后一个子节点hasChildNodes()
元素是否有子节点ownerDocument
指向文档节点的指针,全等于(===) document
let len = someNode.childNodes.length
someNode.childNodes[len - 1] === someNode.lastChild
someNode.childNodes[0] === someNode.firstChild
// 查询某个节点是否有子节点
someNode.hasChildNodes() // true or false
# 操作节点
节点的关系指针都是只读的,DOM 又提供了一些操作节点的方法:
appendChild(节点)
向元素的 childNodes 列表末尾添加节点,返回值为新增加的节点。如果把传入的是已有的节点,会将该节点从原来的位置转移到新位置。insertBefore(newNode, null/oldNode)
如果想把节点插入指定位置,可以使用该函数。新节点会插入到第二个参数节点前面。如果为 null 就等价于 appendChild。replaceChild(newNode, replaceNode)
将要替换的节点,换成新的节点。被替换的节点从 document 中移除。removeChild()
删除节点,删除成功后返回被删除的节点。
其他方法:
cloneNode(是否深复制)
复制一个相同的副本,这个副本属于文档所有,但没有指定父节点,也称为孤儿节点(orphan)。参数为 true 时执行深复制,复制节点及整个节点树,false 时只复制调用该方法的节点本身。normalize()
处理文本节点,检测调用节点的所有后代,如果有空文本节点就删除,如果是兄弟文本节点,就合并为一个文本节点。
// 在末尾新增一个子节点 appendChild() 用于向childNodes列表的末尾添加一个节点
let returnedNode = someNode.appendChild(newNode);
alert(returnedNode === newNode); // true
alert(someNode.lastChild === newNode); // true
// 如果appendChild()操作的节点已经是文档的一部分了,会将原来的节点转移到新的位置
let returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode === someNode.firstChild); // false
alert(returnedNode === someNode.lastChild); // true
// 在指定位置插入节点 insertBefore()
// 插入后成为最后一个子节点
someNode.insertBefore(newNode,null);
// 插入后成为第一个子节点
someNode.insertBefore(newNode,someNode.firstChild);
// 插入到最后一个子节点前面
someNode.insertBefore(newNode,someNode.lastChild);
// 替换节点 replaceChild(要插入的节点,要替换的节点), 返回被替换的节点
someNode.replaceChild(newNode, someNode.firstChild); // 替换第一个子节点
someNode.replaceChild(newNode, someNode.lastChild); // 替换最后一个子节点
// 移除节点
someNode.removeChild(someNode.firstChild); // 移除第一个子节点
someNode.removeChild(someNode.lastChild); // 移除最后一个子节点
// myList
//<ul>
// <li>item 1</li>
// <li>item 2</li>
// <li>item 3</li>
//</ul>
let deepList = myList.cloneNode(true);
deepList.chilidNodes.length // 3,如果是cloneNode(false)则为0,不包含子节点
let shallowList = myList.cloneNode(false) // 浅复制,只是复制节点本身,不会复制子节点
shallowList.childNodes.length // 0
# Document类型(9) document
(Node.DOCUMENT_NODE 9)
Document 类型表示文档节点类型,在浏览器中 document
对象是 HTMLDocument 的一个实例,它继承自 Document 类型,表示整个 html 页面,document 对象是 window 的一个属性,可以全局使用。Document 类型的节点具有以下特征
- nodeType 的值为 9 Node.DOCUMENT_NODE
- nodeName 为 "#document", nodeValue === null , parentNode === null ownerDocument === null
- 在浏览器中,其子节点一般是一个DocumentType(最多一个<!DOCTYPE html>),Element类型(最多一个<html>)
- 一般文档类型的对象是只读的
# 文档子节点
// 如下页面
// <!DOCTYPE html>
// <html>
// <body>
// </body>
// </html>
let html = document.documentElement; // <html> 标签元素
alert(html === document.childNodes[1]); //true
alert(html === document.firstChild); // false 0 为<!docytype html>
let body = document.body;
let doctype = document.doctype; // 如果有就是<!docytype html>,如果头部没写,就是null
# 文档信息
console.log(document.title); // 获取标题
document.title = "JS高程"; // 设置标题
// 获取完整URL
let url = document.URL;
// 取得域名
let domain = document.domain;
// 来源的URL,如果是直接进入,则为"",
// 如果从https://github.com/zuoxiaobai?tab=repositories进入到当前页面
let referrer = document.referrer;
// 如果是上面的场景,则值未"https://github.com/zuoxiaobai?tab=repositories"
# 定位/查找元素
- document.getElementById('kk') 获取文档中第一个 id 为 kk 的元素,类型为 HTMLElement
- document.getElementsByTagName('div') 获取页面所有的div元素 HTMLCollection,和 NodeList 类型,是活动对象
- document.getElementsByName('kk') 获取name='kk' 的所有元素 HTMLCollection
- document.getElementsByClassName('kk') 获取class='kk'的所有元素 HTMLCollection
// <div id="myDiv">Some text</div>
let div = document.getElementById("myDiv"); // 如果没有该id的元素,值为null
div instanceof HTMLElement // true
let div2 = document.getElementsByTagName('div');
div2 instanceof HTMLCollection // true HTMLElement数组
div2[0] instanceof HTMLElement // true
// <img src="1.png" height="500" width="300">
// <img src="2.png" name="myImg" height="500" width="300">
let images = document.getElementsByTagName('img');
alert(images.length); // 2
alert(images[0].src);
alert(images.item(0).src); // src内容
imgs.namedItem('myImg') === imgs["myImg"] // 2.png那张图片
// <text id='t' name="color">textkkkk</text>
document.getElementsByName('color') // 获取name属性为color的元素
# 特殊集合
document 对象上还暴露了几个特殊的集合,这些集合也是 HTMLCollection 的实例
- document.anchors 获取文档中所有带 name 特性的 a 元素,必须要有 name 属性
- document.forms 相当于 document.getElementsByTagName('form')
- document.images 相当于 document.getElementsByTageName('img')
- document.links 获取所有a元素,相当于 documet.getElmentsByTagName('a')
# DOM 兼容性检测
检测浏览器是否支持某些 DOM 功能及版本等信息 p412
# 文档写入
document 对象有一个古老的能力,就是向网页流中写入内容,document.write() document.writeln() 向文档中输入内容,他们的参数都是字符串。write() 只是写入文本,writeln() 除了写入文本外还会在字符串末尾追加一个换行符 \n
<!-- 使用document.write()在页面呈现的过程中直接向其中输入了内容-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>document.write() Title</title>
</head>
<body>
<p>The current date and time is:
<script>
document.write("<strong>"+(new Date()).toLocaleString()+"</strong>");
</script></p>
</body>
</html>
在文档加载结束后,再调用document.write()会覆盖、重写整个页面的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>document.write() Title</title>
</head>
<body>
<p>The current date and time is</p>
<script>
window.onload = function() {
document.write("hello world");
}
</script>
</body>
</html>
# DocumentType类型(10)
(10 Node.DOCUMENT_TYPE_NODE)
DocumentType 包含着与文档的 doctype 有关的所有信息 document.firstChild => <!DOCTYPE html>
,document.doctype
- nodeType 的值为 10 Node.DOCUMENT_TYPE_NODE
- nodeName 的值为 doctype 类型 'html'
- nodeValue 的值为 null
- 父节点 document, 不支持子节点
let e = document.firstChild // <!DOCTYPE html>
e.nodeType // 10
e.nodeName // html
e.nodeValue // null
# Element类型(1)
(1 Node.ELEMENT_NODE)
除 Document 类型外,Element 类型算是 Web 编程中最常用的类型了,它用于表现 XML 或 HTML 元素,提供了对元素标签名、子节点及特性的访问
- nodeType 的值为 1
- nodeName 的值为元素的标签名, nodeValue === null , parentNode 可能是 Document 或 Element 对象
- 访问元素的标签名可以使用 nodeName 属性,也可以使用 tagName 属性,两个属性会返回相同的值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="myDiv"></div>
<script>
let div = document.getElementById("myDiv");
// 在HTML中,标签名始终都以全部大写表示
alert(div.tagName); // DIV
alert(div.tagName === div.nodeName); // true
</script>
</body>
</html>
# HTML元素属性
所有的 HTML 元素都由 HTMLElement 类型或其子类型表示,HTMLElement 类型继承自 Element,并添加了一些属性
- id,元素在文档中的唯一标识符
- className 与元素的class属性对应,及元素指定的css类
- title 有关元素的附加说明
- lang 元素内容的语言,很少使用
- dir 语言方向, 值为 "ltr"(left to right,从左至右)或 "rtl"(right to left,从右至左),很少使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="myDiv" class="bg" title="body text" lang="en" dir="rtl">abcdefg</div>
<script>
let div = document.getElementById("myDiv");
// 在HTML中,标签名始终都以全部大写表示
alert(div.id); // myDiv
alert(div.className); // bg
alert(div.title); // body text
alert(div.lang); // en
alert(div.dir); // rtl 这里注意,文字会在屏幕右边显示,类似于右对齐。从右边开始显示
// 也可以直接修改属性, 立即生效
div.id = "someOther";
div.className = "ft";
div.title = "Some other text";
div.lang = "ch";
div.dir = "ltr";
</script>
</body>
</html>
HTML元素以及与之关联的类型,斜体表示已经废弃的
# 获取/设置属性/特性
每个元素都有零个或多个属性,用于为元素或其内容附加更多信息。相关的 DOM 方法主要有三个:
- getAttribute(prop) 获取元素的属性名,prop 属性名,属性名不区分大小写
- setAttribute(prop, value) 设置元素的属性
- removeAttribute(prop) 移除元素属性
let div = document.getElementById("myDiv");
div.id // myDiv
div.getAttribute('id') // myDiv
// 设置特性值
div.setAttribute('id', 'test') // 等价于 div.id = 'test'
div.removeAttribute('class') // 删除class特性
# attributes属性
可以用来增删查改特性,主要用来遍历某个元素的特性。元素的 attributes 属性是 NamedNodeMap 对象,是类似与 NodeList 的活动对象,支持数组下标,item(),forEach 遍历或访问对应的值。它里面的值都是 属性(特性)节点 Node.ATTRIBUTE_NODE(2),NamedNodeMap 对象包含如下方法:
- getNamedItem(prop) 获取属性 prop 的属性节点
- removeNameItem(prop) 移除 prop 属性
- setNamedItem(node) 向列表中添加 node 节点
- item(pos) 返回 pos 索引位置的节点
// 获取某个节点的属性列表, 类型为 NamedNodeMap
// <div id="myDiv" class="bg" title="body text" lang="en" dir="rtl">abcdefg</div>
let myDiv = document.getElementById('myDiv')
let s = myDiv.attributes
// NamedNodeMap {0: id, 1: class, 2: lang, 3: dir, id: id, class: class, lang: lang, dir: dir, length: 4}
s.getNamedItem('id').nodeName // id
s.getNamedItem('id').nodeValue // myDiv
s['title'].nodeValue = 'xxx' // 设置title特性值为xxx
s[0].nodeValue // myDiv
s.removeNamedItem('id') // 移除id的特性,相当于 s.removeAttribute('id')
# 创建元素
document.createElement('元素名称')
// 创建一个div元素
// <div class="ft">footer</div>
k = document.createElement('div')
k.id = 'id2'
k.innerText = '1212'
document.getElementsByClassName('ft')[0].appendChild(k)
// <div class="ft">footer<div id='id2'>1212</div></div>
childNodes 属性包含元素的所有子节点:可能是元素节点(1)、文本节点(3)、注释(8)等,如果是找元素节点,我们再遍历时需要判断下 nodeType 是否为 1
# Attr类型(2)
(2 Node.ATTRIBUTE_NODE)
特性节点类型,element.attributes 就是特性节点数组,子元素就是特性节点
- nodeType 的值为 2
- nodeName 的值为特性的名称,比如 id
- nodeValue 的值为特性的值,比如 xx
- parentNode 的值为 null
- 不支持子节点
let attr = document.createAttribute('align');
attr.value = 'left';
attr.nodeType // 2
attr.nodeName // align
attr.nodeValue // left
let element = document.createElement('div');
element.setAttribute(attr);
element.attributes['align'].value // left
element.getAttributeNode('align').value // left
element.getAttribute('align') // left
属性作为节点来访问,多数情况下是没必要的,推荐使用 getAttribute()、removeAttribute()、setAttribute() 方法操作属性
# Text 类型(3)
(4 Node.TEXT_NODE)
文本节点有 Text 类型表示,不能包含 HTML 代码,不支持子节点
- nodeType的值为 3 Node.TEXT_NODE
- nodeName 的值为 '#text'
- nodeValue 的值为节点所包含的文本
Text 节点暴露了以下属性和方法
appendData(text)
向节点末尾添加文本 textdeleteData(offset, count)
从 offset 位置开始,删除 count 个字符insertData(offset, text)
从 offset 位置插入 textreplaceData(offset, count, text)
用 text 替换从 offset 位置开始到 offset + count 位置的文本splitText(offset)
在 offset 位置将当前文本节点拆分为两个节点subStringData(offset, count)
提取从位置 offset 到 offset + count 位置的文本normalize()
规范化文本节点,合并多个子文本节点length
文本包含的字符数量
创建文本节点
document.createTextNode(text)
创建文本节点
// <div id='someOther' class='ft' title='Some other text'>abcdefg</div>
let oth = document.getElementById('someOther')
let textNode = oth.childNodes[0] // Text()
textNode.nodeType // 3
textNode.nodeName // "#text"
textNode.nodeValue // "abcdeft"
textNode.nodeValue = 'ddd' // 修改text的值
// 代码创建一个文本节点,挂载到元素节点div上,再放到body里面
let e = document.createElement('div')
e.className = 'message'
let tNode = document.createTextNode('hello world')
e.appendChild(tNode)
// 一个div可以多增加几个textNode,中间不会有空格,会连着显示。
let tNode2 = document.createTextNode('---hello world---')
e.appendChild(tNode2)
document.body.appendChild(e)
e.childNodes.length // 2
e.normalize() // 规范化文本节点
e.childNodes.length // 1
// 再分割文本节点
let newNode = e.firstChild.splitText(5)
e.firstChild.nodeValue // 'hello'
newNode.nodeValue // " world---hello world---"
e.childNodes.length // 2
# Comment 类型(8)
(8 Node.COMMENT_NODE)
注释类型,创建注释类型:document.createComment('注释内容')
- nodeType 的值为 8 Node.COMMENT_NODE
- nodeName 的值为 '#comment'
- nodeValue 的值为 注释类型 等价于
.data
let divEle = document.createElement('div');
let comNode = document.createComment('测试注释类型');
divEle.id = "comDiv";
divEle.appendChild(comNode);
comNode.nodeType // 8
comNode.nodeName // "#comment"
comNode.nodeValue // 测试注释类型
comNode.data // 测试注释类型,以上两种都可以修改注释内容
# CDATASection 类型(4)
XML CDATA 相关,浏览器不支持,略。详情参见 423
# DocumentFragment(11)
(11 Node.DOCUMENT_FRAGMENT_NODE)
文档片段类型,不会真正的再文档里形成节点,类似与一个中转节点。
- nodeType 的值为 11
- nodeNmae 的值为 "#document-fragment"
- nodeValue 的值为 null
- parentNode 的值为 null
let fragment = document.createDocumentFragment();
let ul = document.createElement('ul');
for (let i = 0; i < 3; i++) {
let li = document.createElement('li');
li.appendChild(document.createTextNode(`text ${i}`));
fragment.appendChild(li)
}
ul.appendChild(fragment);
document.body.appendChild(ul);
ul.childNodes // 只有3个 li 子节点
# DOM编程/操作技术
DOM 操作往往是 JS 程序中开销最大的部分,NodeList 对象是动态的,每次访问 NodeList 对象,都会运行一次查询,尽量少 DOM 操作
# 动态添加脚本script
// <script type='text/javascript' src='text.js'></script>
// test.js 里面 alert('test dynamic script')
// 添加一个script脚本元素节点
let scriptNode = document.createElement('script');
scriptNode.src = 'test.js';
// scriptNode.type = 'text/javascript'; 第 4 版省略
document.body.appendChild(scriptNode);
// 上面是载入一个 js 文件,下面是以行内方式加载 js 代码
let sNode = document.createElement('script');
sNode.type = 'text/javascript';
// sNode.appendChild(document.createTextNode('function sayHi() { alert("Hi"); }'));
sNode.text = 'function sayHi() {alert("Hi");} sayHi()'; // 兼容IE写法
document.body.appendChild(sNode);
// 封装成函数
function loadScriptString(scriptStr) {
let script = document.createElement('script');
script.type = 'text/javascript';
try {
script.appendChild(document.createTextNode(scriptStr));
} catch (e) {
// IE下可能会失效,走这里的逻辑
script.text = scriptStr;
}
document.body.append(script);
}
loadScriptString('function sayHi() { alert("Hi"); } sayHi()');
// 等效于
eval('function sayHi() { alert("Hi"); } sayHi()');
通过 innerHTML 属性创建的 <script>
元素永远不会执行。以后也没法强制执行。
# 动态添加样式style
// <link rel="stylesheet" type="text/css" href="test.css">
let linkNode = document.createElement('link');
linkNode.rel = 'stylesheet';
linkNode.setAttribute('type', 'text/css'); // 熟悉下语法
linkNode.href = 'test.css';
document.head.appendChild(linkNode);
// document.head 等价于 document.getElementsByTagName('head')[0]
// text node
// <style>
// body { color: red }
// </style>
function loadStyleString(cssStr) {
let styleNode = document.createElement('style');
try {
styleNode.appendChild(document.createTextNode(cssStr));
} catch (e) {
// IE 不允许动态添加 script、style标签子节点,会报异常,需要特殊处理
styleNode.styleSheet.cssText = cssStr;
}
document.body.appendChild(styleNode);
}
loadStyleString('body {color: red;}');
# 操作表格HTML-DOM
动态创建一个表格,html 代码如下,先利用 DOM API 来动态创建。再利用 HTML-DOM 提供的 table、tbody、tr 方法来重构
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>表格</title>
</head>
<body>
<table border="1" width="100%">
<tbody>
<tr>
<td>Cell 1,1</td>
<td>Cell 1,2</td>
</tr>
<tr>
<td>Cell 2,1</td>
<td>Cell 2,2</td>
</tr>
</tbody>
</table>
</body>
</html>
js实现
let tableNode = document.createElement('table');
tableNode.border = '1';
tableNode.width = '100%';
let tbodyNode = document.createElement('tbody');
// 添加第一行
let tr1 = document.createElement('tr');
let td11 = document.createElement('td');
td11.appendChild(document.createTextNode('Cell 1,1'));
let td12 = document.createElement('td');
td12.appendChild(document.createTextNode('Cell 1,2'));
tr1.appendChild(td11);
tr1.appendChild(td12);
// 添加第二行
let tr2 = document.createElement('tr');
let td21 = document.createElement('td');
td21.appendChild(document.createTextNode('Cell 2,1'));
let td22 = document.createElement('td');
td22.appendChild(document.createTextNode('Cell 2,2'));
tr2.appendChild(td21);
tr2.appendChild(td22);
tbodyNode.appendChild(tr1);
tbodyNode.appendChild(tr2);
tableNode.appendChild(tbodyNode);
document.body.appendChild(tableNode);
HTML DOM为方便创建表格提供的 table、tbody、tr 属性及方法
<table border="1" width="100%">
<caption>表格的标题</caption>
<thead>
<tr>
<th>列1</th>
<th>列2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell 1,1</td>
<td>Cell 2,1</td>
</tr>
<tr>
<td>Cell 1,1</td>
<td>Cell 2,1</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan='2'>table foot</td>
</tr>
</tfoot>
</table>
// 先获取table元素
let tableEle = document.getElementsByTagName('table')[0];
// table元素节点 相关属性
tableEle.caption // 获取caption元素节点,如果没有,则返回null
tableEle.tBodies // <tbody>元素的数组, HTMLCollection tableEle.tBodies[0] 一个tbody元素
tableEle.tFoot // 获取tFoot元素节点
tableEle.tHead // 获取tHead元素节点
tableEle.rows // 获取表格里的tr数组,包含thead、tfoot,4个 tableEle.rows[0] tr节点
// table元素节点 相关方法insert
tableEle.createTHead() // 如果table内已有<thead>,返回对应的thead元素节点,如果没有,创建<thead></thead>并添加到表格,返回其引用
tableEle.createTFoot() // 如果table内已有<tfoot>,返回对应的tfoot元素节点,如果没有,创建<tfoot></tfoot>并添加到表格,返回其引用
tableEle.createCaption() // 如果table内已有<caption>,返回对应的节点,如果没有,创建<caption></caption>并添加到表格,返回其引用
tableEle.deleteTHead() // 删除<thead>元素节点
tableEle.deleteTFoot() // 删除<tfoot>元素节点
tableEle.deleteCaption() // 删除<caption>元素节点
tableEle.deleteRow(pos) // 删除rows里面的某一行,从0开始,tableEle.deleteRow(0),删除第一个tr节点
tableEle.insertRow(pos) // 创建一个<tr>添加到table里,并返回其引用,如果pos不传值,添加到末尾,(tfoot里面),tableEle.insertRow(0) 在首部添加tr
// tbody 元素的属性及方法
let tbodyNode = tableEle.tBodies[0];
tbodyNode.rows // <tbody> 里的元素数组 HTMLColletion, length = 2
tbodyNode.deleteRow(pos) // 删除指定位置tr节点
tbodyNode.insertRow(pos) // 在指定位置插入节点
// tr 元素属性及方法
let trNode = tbodyNode.firstElementChild; // 第一个元素节点
trNode.cells // 保存着tr里面的单元格列表元素,HTMLCollection <td> List
trNode.deleteCell(pos) // 删除指定位置的单元格
trNode.insertCell(pos) // 指定位置添加<td></td>
用table相关的HTML-DOM API重写之前创建table的例子
let tableNode = document.createElement('table');
tableNode.border = '1';
tableNode.width = '100%';
let tbodyNode = document.createElement('tbody');
// 添加第一行
let tr1 = tbodyNode.insertRow(0);
let td1 = tr1.insertCell(0);
let td2 = tr1.insertCell(1);
td1.appendChild(document.createTextNode('Cell 1,1'));
td2.appendChild(document.createTextNode('Cell 1,2'));
// 添加第二行
let tr2 = tbodyNode.insertRow(1);
let td3 = tr2.insertCell(0);
let td4 = tr2.insertCell(1);
td3.appendChild(document.createTextNode('Cell 2,1'));
td4.appendChild(document.createTextNode('Cell 2,2'));
tableNode.appendChild(tbodyNode);
document.body.appendChild(tableNode);
# NodeList
理解 NodeList 对象和相关的 NamedNodeMap、HTMLCollection 是理解 DOM 编程的关键。这三个集合都是 "实时的",也叫 "活动对象"。节点是动态的,他们始终是最新的。
下面的例子中,获取了某个节点元素,每次访问其 length 时,元素都有动态更新,length 也会动态增加是个死循环,为了提高性能,尽量少操作dom
// <div>123</div>
let divs = document.getElementsByTagName('div'), i, div;
for (let i = 0; i < divs.length; i++) {
div = document.createElement('div');
console.log(i) // 死循环
document.body.appendChild(div);
}
// 改写上面的死循环,这样就只执行有限的个数了。
let divs = document.getElementsByTagName('div'), i, div;
let length = 0;
for (i = 0, length= divs.length; i < length; i++) {
div = document.createElement('div');
document.body.appendChild(div);
}
最好限制操作 NodeList 的次数,或者把 NdoeList 的结果缓存起来。
# MutationObserver 接口
MutationObserver 接口是 DOM 新增的规范。它可以在 DOM 被修改时异步执行回调。可以观察整个文档或 DOM 树的一部分,或某个元素的变化。
MutationObserver 实例需要通过 MutationObserver 构造函数并传入一个回调函数来创建
let observer = new MutationObserver(console.log)
observer // MutationObserver {}
新创建的 MutationObserver 实例不会关联 DOM 的任何部分,需要使用 observe() 方法来关联 DOM。当 DOM 发生指定变化时会触发回调函数。这里的回调函数和 Promise.then 类似,是微任务。
MutationObserver 实例包含以下几个方法:
observe(要观察的dom节点, MutationObserverInit 对象)
,设置需要观察的 DOM,以及观察的内容。MutationObserverInit 对象用于控制观察 DOM 哪方面的变化。- takeRecords() 可以清空记录队列,取出并返回其中的所有 MutationRecord 实例
- disconnect() 停止观察,提前终止执行回调。并抛弃已经加入微任务队列的回调。
let observer = new MutationObserver(console.log)
// 监听 document.body 元素上属性变化,其子节点变更不会触发 回调
observer.observe(document.body, { attributes: true })
document.body.className = 'test'
console.log('changed body class')
// changed body class
// [MutationRecord]0: MutationRecord {type: "attributes", target: body.test, addedNodes: NodeList(0), removedNodes: NodeList(0), previousSibling: null, …}length: 1__proto__: Array(0)
// MutationObserver {}
// MutationRecord 展开
// {
// addedNodes: NodeList []
// attributeName: "class"
// attributeNamespace: null
// nextSibling: null
// oldValue: null
// previousSibling: null
// removedNodes: NodeList []
// target: body.test
// type: "attributes"
// __proto__: MutationRecord
// }
上面的例子中我们可以看到,当属性发生变化后,触发了 MutationObserver 实例的回调函数,且回调函数的第一个参数是 MutationRecord 实例的数组(因为回调函数执行之前,可能同时发生了多个满足观察条件的事件,所以是数组)。第二个参数是 MutationObserver 实例。下面是 MutationRecord 数组的例子:
let observer = new MutationObserver(console.log)
observer.observe(document.body, { attributes: true })
document.body.className = 'a'
document.body.className = 'b'
document.body.className = 'c'
console.log('changed body class')
// 打印信息如下,回调函数第一个参数是 mutationRecords
// 第二个参数是 MutationObserver 实例
// [MutationRecord, MutationRecord, MutationRecord]
// MutationObserver {}
MutationRecord 实例包含的信息包括发生了什么变化,以及 DOM 的哪一部分受了影响。它包含如下实例属性
target
被修改影响的目标节点,比如上面例子中的 body 元素type
字符串,表示监听变化的类型,比如: "attributes"、"characterData" 或 "childList"oldValue
旧值,如果 type 是 attributes 或 characterData,在 MutationObserverInit 对象中设置了 attributeOldValue 或 characterDataOldValue 为 true,就会保存旧的值,否则为 nullattributeName
type 为 "attributes" 时,保存被修改的属性名attributeNamespace
type 为 "attributes" 时,被修改的命名空间属性名addedNodes
type 为 "childList" 时,返回包含变化中添加节点的 NodeList,默认为空 NodeListremoveNodes
type 为 "childList" 时,返回包含变化中删除节点的 NodeList,默认为空 NodeListpreviousSibling
type 为 "childList" 时,返回变化节点的前一个兄弟 Node,默认为 nullnextSibling
type 为 "childList" 时,返回变化节点的后一个兄弟 Node,默认为 null
disconnect()方法 下面的例子中,不会有任何内容输出,如果想要让已加入任务队列的回调执行。可以在使用 disconnect 时,加上 setTimeout
let observer = new MutationObserver(console.log)
observer.observe(document.body, { attributes: true })
document.body.className = 'foo' // 将要触发的回调加入微任务队列,继续向下执行
observer.disconnect() // 停止观察,并将已加入微任务队列的回调清空
document.body.className = 'bar' // 已停止观察,无法触发
let observer = new MutationObserver(console.log)
observer.observe(document.body, { attributes: true })
document.body.className = 'foo' // 将要触发的回调加入微任务队列,继续向下执行
setTimeout(() => {
// 将下面的内容加入宏任务队列,等待执行,这个时候会将微任务队列的回调执行
observer.disconnect() // 停止观察,并将已加入微任务队列的回调清空
document.body.className = 'bar' // 已停止观察,无法触发
}, 0)
// 会触发一次回调
// [MutationRecord] MutationObserver {}
MutationObserver 实例可以复用,observer() 一个 DOM 后,再次调用 observer() 观察另一个 DOM 也是可以的。通过 MutationRecord 的 target 可以区分是哪个 DOM 发生的变化。disconnect() 后,两个 DOM 都会停止观察。
注意:使用 disconnect() 后,再次调用 observer() 开始监听 DOM,是可以继续监听,并触发回调的。详情参见:p437
# MutationObserverInit与观察范围
MutationObserverInit 对象用于控制观察范围,他的属性如下
subtree
布尔值,默认 false。是否观除了观察目标节点外,还观察目标节点的子树,后代子节点attributes
布尔值,默认 false。是否观察目标节点属性变化attributeFilter
字符串数组,表示要观察哪些属性变化。如果值为 true,会将 attributes 设置为 true,观察所有属性attributeOldValue
布尔值,默认 false,表示 MutationRecord 是否记录旧的值。如果为 true,也会将 attributes 转换为 truecharacterData
布尔值,默认为 false,表示修改字符数据是否触发变化事件characterDataOldValue
布尔值,默认 false,表示 MutationRecord 是否记录旧的值,如果为 true,会将 characterData 转换为 truechildList
布尔值,默认为 false,表示修改目标节点的子节点是否触发变化事件,
# 观察属性
let observer = new MutationObserver(console.log)
observer.observe(document.body, { attributeOldValue: true })
document.body.setAttribute('foo', 'a')
document.body.setAttribute('foo', 'b')
document.body.removeAttribute('foo')
// 以上 添加属性、修改属性、移除属性都被记录下来了,oldValue 分别为 null, 'a', 'b'
// [MutationRecord, MutationRecord, MutationRecord]
// MutationObserver {}
let observer = new MutationObserver(console.log)
observer.observe(document.body, { attributeFilter: ['foo'] })
document.body.setAttribute('foo', 'a') // 添加白名单属性
document.body.setAttribute('bar', 'b') // 添加非白名单属性
// [MutationRecord] 只有白名单内 foo 属性的变化被记录下来
// MutationObserver {}
# 观察字符串数据
characterData 用于观察文本节点(如 Text、Comment 或 ProcessingInstructioin 节点)中字符的添加、修改和删除
let observer = new MutationObserver(console.log)
document.body.innerText = 'a'
observer.observe(document.body.firstChild, { characterDataOldValue: true })
// chrome 浏览器并不会触发回调,相反使用 innerText 后,相当于执行了 observer.disconnect()
document.body.innerText = 'a'
document.body.innerText = 'b'
document.body.firstChild.textContent = 'c'
// 这个时候我再重新 observer,使用 firstChild.textContent 修改才会触发回调
observer.observe(document.body.firstChild, { characterDataOldValue: true })
document.body.firstChild.textContent = 'd'
document.body.firstChild.textContent = 'e'
document.body.firstChild.textContent = 'e'
// 可以监听到最后三次修改,oldValue 分别为 'c','d','e'
// [MutationRecord, MutationRecord, MutationRecord]
// MutationObserver {}
# 观察子节点
childList 为 true 时可以观察目标子节点的添加和移除。对于子节点重新排序会触发两次 MutationRecord,一次移除,一次新增
document.body.innerHTML = ''
let observer = new MutationObserver(console.log)
observer.observe(document.body, { childList: true })
let element = document.createElement('div')
document.body.appendChild(element)
// 检测到新增节点
// [MutationRecord {
// type: "childList",
// target: body,
// addedNodes: NodeList [div],
// // ...
// }]
document.body.removeChild(element)
// 检测到移除节点
// [MutationRecord {
// type: "childList",
// target: body,
// removedNodes: NodeList [div]
// // ...
// }]
document.body.appendChild(document.createElement('div'))
document.body.appendChild(document.createElement('span'))
// 触发两次新增事件
// 再修改子节点顺序
document.body.insertBefore(document.body.lastChild, document.body.firstChild)
// [MutationRecord, MutationRecord]
// MutationObserver {}
// 上面触发了两次事件,一次移除末尾 span 节点,一次添加 span 节点
// MutationRecord { previousSibling: div,removedNodes: NodeList [span] }
// MutationRecord { addedNodes: NodeList [span], nextSibling: div }
# 观察子树
subtree 可以把观察范围扩大到观察子树。另外被观察的子树节点,被移除子树后,仍然可能触发变动事件。
document.body.innerHTML = ''
let observer = new MutationObserver(console.log)
document.body.appendChild(document.createElement('div'))
observer.observe(document.body, { attributes: true, subtree: true })
document.body.firstChild.setAttribute('foo', 'a')
// [MutationRecord] MutationObserver {}
// MutationRecord { target: div, attributeName: "foo", type: "attributes"}
# 异步回调与记录队列
MutationObserver 接口是出于性能考虑而设计的,核心是异步回调与记录队列模型。为了在大量变化事件发生时,不影响性能,每次变化的信息会保存在 MutationRecord 实例中,然后添加到 记录队列,队列中每个 MutationObserver 实例都是唯一的,是所有 DOM 变化事件的有序列表。
记录队列是 微任务。
调用 MutationObserver 实例的 takeRecords() 可以清空记录队列,取出并返回其中的所有 MutationRecords 实例
let observer = new MutationObserver((mutationRecords) => {
console.log('出发了回调', mutationRecords)
})
observer.observe(document.body, { attributes: true })
document.body.setAttribute('class', 'a')
document.body.setAttribute('class', 'b')
document.body.setAttribute('class', 'c')
console.log(observer.takeRecords())
console.log(observer.takeRecords())
// 并没有触发回调,而是被 observer.takeRecords() 取出并清空
// [MutationRecord, MutationRecord, MutationRecord]
// []
# 性能、内存与垃圾回收
DOM Level 2 规范中的 MutationEvent 可以监听 dom 变化事件,它出现了严重的性能问题,在 DOM Level 3 中被废弃,取而代之的是 MutationObserver 接口。
注意:MutationObserver 实例拥有目标节点的弱引用。目标节点拥有 MutationObserver 实例的强引用。如果目标节点从 dom 中移除,MutationObserver 也会被垃圾回收。
MutationRecord 实例会保存他们引用的节点,会妨碍节点被回收,建议使用时从 MutationRecord 中拷贝最有用的信息到一个新的对象再使用。