You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
13 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 浏览器DOM你知道HTML的节点有哪几种吗
你好我是winter。
今天我们进入浏览器API的学习, 这一节课我们来学习一下DOM API。
DOM API是最早被设计出来的一批API也是用途最广的API所以早年的技术社区常常用DOM来泛指浏览器中所有的API。不过今天这里我们要介绍的DOM指的就是狭义的文档对象模型。
## DOM API介绍
首先我们先来讲一讲什么叫做文档对象模型。
顾名思义文档对象模型是用来描述文档这里的文档是特指HTML文档也用于XML文档但是本课不讨论XML。同时它又是一个“对象模型”这意味着它使用的是对象这样的概念来描述HTML文档。
说起HTML文档这是大家最熟悉的东西了我们都知道HTML文档是一个由标签嵌套而成的树形结构因此DOM也是使用树形的对象模型来描述一个HTML文档。
DOM API大致会包含4个部分。
* 节点DOM树形结构中的节点相关API。
* 事件触发和监听事件相关API。
* Range操作文字范围相关API。
* 遍历遍历DOM需要的API。
事件相关API和事件模型我们会用单独的课程讲解所以我们本篇文章重点会为你介绍节点和遍历相关API。
DOM API 数量很多我希望给你提供一个理解DOM API设计的思路避免单靠机械的方式去死记硬背。
### 节点
DOM的树形结构所有的节点有统一的接口Node我们按照继承关系给你介绍一下节点的类型。
![](https://static001.geekbang.org/resource/image/6e/f6/6e278e450d8cc7122da3616fd18b9cf6.png)
在这些节点中除了Document和DocumentFrangment都有与之对应的HTML写法我们可以看一下。
```
Element: <tagname>...</tagname>
Text: text
Comment: <!-- comments -->
DocumentType: <!Doctype html>
ProcessingInstruction: <?a 1?>
```
我们在编写HTML代码并且运行后就会在内存中得到这样一棵DOM树HTML的写法会被转化成对应的文档模型而我们则可以通过JavaScript等语言去访问这个文档模型。
这里我们每天都需要用到要重点掌握的是Document、Element、Text节点。
DocumentFragment也非常有用它常常被用来高性能地批量添加节点。因为Comment、DocumentType和ProcessingInstruction很少需要运行时去修改和操作所以有所了解即可。
### Node
Node是DOM树继承关系的根节点它定义了DOM节点在DOM树上的操作首先Node提供了一组属性来表示它在DOM树中的关系它们是
* parentNode
* childNodes
* firstChild
* lastChild
* nextSibling
* previousSibling
从命名上我们可以很清晰地看出这一组属性提供了前、后、父、子关系有了这几个属性我们可以很方便地根据相对位置获取元素。当然Node中也提供了操作DOM树的API主要有下面几种。
* appendChild
* insertBefore
* removeChild
* replaceChild
这个命名跟上面一样我们基本可以知道API的作用。这几个API的设计可以说是饱受诟病。其中最主要的批评是它不对称——只有before没有after而jQuery等框架都对其做了补充。
实际上appendChild和insertBefore的这个设计是一个“最小原则”的设计这两个API是满足插入任意位置的必要API而insertAfter则可以由这两个API实现出来。
我个人其实不太喜欢这个设计对我而言insertAt(pos) 更符合审美一些。当然,不论喜不喜欢,这个标准已经确定,我们还是必须要掌握它。
这里从设计的角度还想要谈一点那就是所有这几个修改型的API全都是在父元素上操作的比如我们要想实现“删除一个元素的上一个元素”必须要先用parentNode获取其父元素。
这样的设计是符合面向对象的基本原则的。还记得我们在JavaScript对象部分讲的对象基本特征吗“拥有哪些子元素”是父元素的一种状态所以修改状态应该是父元素的行为。这个设计我认为是DOM API中好的部分。
到此为止Node提供的API已经可以很方便大概吧地对树进行增、删、遍历等操作了。
除此之外Node还提供了一些高级API我们来认识一下它们。
* compareDocumentPosition 是一个用于比较两个节点中关系的函数。
* contains 检查一个节点是否包含另一个节点的函数。
* isEqualNode 检查两个节点是否完全相同。
* isSameNode 检查两个节点是否是同一个节点实际上在JavaScript中可以用“===”。
* cloneNode 复制一个节点如果传入参数true则会连同子元素做深拷贝。
DOM标准规定了节点必须从文档的create方法创建出来不能够使用原生的JavaScript的new运算。于是document对象有这些方法。
* createElement
* createTextNode
* createCDATASection
* createComment
* createProcessingInstruction
* createDocumentFragment
* createDocumentType
上面的这些方法都是用于创建对应的节点类型。你可以自己尝试一下。
## Element 与 Attribute
Node提供了树形结构上节点相关的操作。而大部分时候我们比较关注的是元素。Element表示元素它是Node的子类。
元素对应了HTML中的标签它既有子节点又有属性。所以Element子类中有一系列操作属性的方法。
我们需要注意对DOM而言Attribute和Property是完全不同的含义只有特性场景下两者才会互相关联这里在后面我会详细讲解今天的文章里我就不展开了
首先我们可以把元素的Attribute当作字符串来看待这样就有以下的API
* getAttribute
* setAttribute
* removeAttribute
* hasAttribute
如果你追求极致的性能还可以把Attribute当作节点
* getAttributeNode
* setAttributeNode
此外如果你喜欢property一样的访问attribute还可以使用 attributes 对象,比如 document.body.attributes.class = “a” 等效于 document.body.setAttribute(“class”, “a”)。
### 查找元素
document节点提供了查找元素的能力。比如有下面的几种。
* querySelector
* querySelectorAll
* getElementById
* getElementsByName
* getElementsByTagName
* getElementsByClassName
我们需要注意getElementById、getElementsByName、getElementsByTagName、getElementsByClassName这几个API的性能高于querySelector。
而 getElementsByName、getElementsByTagName、getElementsByClassName 获取的集合并非数组,而是一个能够动态更新的集合。
我们看一个例子:
```
var collection = document.getElementsByClassName('winter');
console.log(collection.length);
var winter = document.createElement('div');
winter.setAttribute('class', 'winter')
document.documentElement.appendChild(winter)
console.log(collection.length);
```
在这段代码中我们先获取了页面的className为winter的元素集合不出意外的话应该是空。
我们通过console.log可以看到集合的大小为0。之后我们添加了一个class为winter的div这时候我们再看集合可以发现集合中出现了新添加的元素。
这说明浏览器内部是有高速的索引机制来动态更新这样的集合的。所以尽管querySelector系列的API非常强大我们还是应该尽量使用getElement系列的API。
## 遍历
前面已经提到过通过Node的相关属性我们可以用JavaScript遍历整个树。实际上DOM API中还提供了NodeIterator 和 TreeWalker 来遍历树。
比起直接用属性来遍历NodeIterator 和 TreeWalker 提供了过滤功能,还可以把属性节点也包含在遍历之内。
NodeIterator的基本用法示例如下
```
var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);
var node;
while(node = iterator.nextNode())
{
console.log(node);
}
```
这个API的设计非常老派这么讲的原因主要有两点一是循环并没有类似“hasNext”这样的方法而是直接以nextNode返回null来标志结束二是第二个参数是掩码这两个设计都是传统C语言里比较常见的用法。
放到今天看这个迭代器无法匹配JavaScript的迭代器语法而且JavaScript位运算并不高效掩码的设计就徒增复杂性了。
这里请你注意一下这个例子中的处理方法通常掩码型参数我们都是用按位或运算来叠加。而针对这种返回null表示结束的迭代器我使用了在while循环条件中赋值来保证循环次数和调用next次数严格一致但这样写可能违反了某些编码规范
我们再来看一下TreeWalker的用法。
```
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false)
var node;
while(node = walker.nextNode())
{
if(node.tagName === "p")
node.nextSibling();
console.log(node);
}
```
比起NodeIteratorTreeWalker多了在DOM树上自由移动当前节点的能力一般来说这种API用于“跳过”某些节点或者重复遍历某些节点。
总的来说我个人不太喜欢TreeWalker和NodeIterator这两个API建议需要遍历DOM的时候直接使用递归和Node的属性。
## Range
Range API 是一个比较专业的领域,如果不做富文本编辑类的业务,不需要太深入。这里我们就仅介绍概念和给出基本用法的示例,你只要掌握即可。
Range API 表示一个HTML上的范围这个范围是以文字为最小单位的所以Range不一定包含完整的节点它可能是Text节点中的一段也可以是头尾两个Text的一部分加上中间的元素。
我们通过 Range API 可以比节点 API 更精确地操作 DOM 树,凡是 节点 API 能做到的Range API都可以做到而且可以做到更高性能但是 Range API 使用起来比较麻烦,所以在实际项目中,并不常用,只有做底层框架和富文本编辑对它有强需求。
创建Range一般是通过设置它的起止来实现我们可以看一个例子
```
var range = new Range(),
firstText = p.childNodes[1],
secondText = em.firstChild
range.setStart(firstText, 9) // do not forget the leading space
range.setEnd(secondText, 4)
```
此外,通过 Range 也可以从用户选中区域创建这样的Range用于处理用户选中区域:
```
var range = document.getSelection().getRangeAt(0);
```
更改 Range 选中区段内容的方式主要是取出和插入分别由extractContents和insertNode来实现。
```
var fragment = range.extractContents()
range.insertNode(document.createTextNode("aaaa"))
```
最后我们看一个完整的例子。
```
var range = new Range(),
firstText = p.childNodes[1],
secondText = em.firstChild
range.setStart(firstText, 9) // do not forget the leading space
range.setEnd(secondText, 4)
var fragment = range.extractContents()
range.insertNode(document.createTextNode("aaaa"))
```
这个例子展示了如何使用range来取出元素和在特定位置添加新元素。
## 总结
在今天的文章中我们一起了解了DOM API的内容。DOM API大致会包含4个部分。
* 节点DOM树形结构中的节点相关API。
* 事件触发和监听事件相关API。
* Range操作文字范围相关API。
* 遍历遍历DOM需要的API。
DOM API中还提供了NodeIterator 和 TreeWalker 来遍历树。比起直接用属性来遍历NodeIterator 和 TreeWalker 提供了过滤功能,还可以把属性节点也包含在遍历之内。
除此之外我们还谈到了Range的一些基础知识点这里你掌握即可。
最后我给你留了一个题目请你用DOM API来实现遍历整个DOM树把所有的元素的tagName打印出来。
* * *
### 补充阅读:命名空间
我们本课介绍的所有API特意忽略了命名空间。
在HTML场景中需要考虑命名空间的场景不多。最主要的场景是SVG。创建元素和属性相关的API都有带命名空间的版本
* document
* createElementNS
* createAttributeNS
* Element
* getAttributeNS
* setAttributeNS
* getAttributeNodeNS
* setAttributeNodeNS
* removeAttributeNS
* hasAttributeNS
* attributes.setNamedItemNS
* attributes.getNamedItemNS
* attributes.removeNamedItemNS
若要创建Document或者Doctype也必须要考虑命名空间问题。DOM要求从document.implementation来创建。
* document.implementation.createDocument
* document.implementation.createDocumentType
除此之外,还提供了一个快捷方式,你也可以动手尝试一下。
* document.implementation.createHTMLDocument
# 猜你喜欢
[![unpreview](https://static001.geekbang.org/resource/image/1a/08/1a49758821bdbdf6f0a8a1dc5bf39f08.jpg)](https://time.geekbang.org/course/intro/163?utm_term=zeusMTA7L&utm_source=app&utm_medium=chongxueqianduan&utm_campaign=163-presell)