浏览器DOM:你知道HTML的节点有哪几种吗?
DOM API介绍
文档类型模型用来描述文档,这里的文档,这里特指HTML文档(也包含XML,但本文不讨论XML)。同时它又是一个“对象模型”,这意味着它是以对象这样的概念描述HTML文档。
HTML文档,是由标签嵌套而成的树形结构,因此,DOM也是使用树形的对象模型来描述HTML文档。
DOM API大致分为4个部分:
- 节点:DOM树形结构中的节点相关API。
- 事件:触发和监听事件相关API。
- Range:操作文字范围相关API。
- 遍历:遍历DOM需要的API。
节点
DOM的树形结构所有节点有统一的接口Node。按照继承关系,介绍一下节点的类型。
除了Document和DocumentFragment以外,都有与之对应的HTTP写法:
Element: <tagname>...</tagname>
Text: text
Comment: <!-- comments -->
DocumentType: <!Doctype html>
ProcessingInstruction: <?a 1?>
我们需要关注的是:Document、Element、Text节点。
Node
Node是DOM树继承关系的跟节点,它定义了DOM节点在DOM树上的操作,首先Node提供了一组属性,来表示它在DOM树中的关系:
- parentNode
- childNodes
- firstChild
- lastChild
- nextSibling
- previousSibling
从命名上,可以清晰地看出这组数据提供了前、后、父、子关系,有了这几个属性,我们可以很方便的根据相对位置获取元素。Node中也提供了操作DOM树的API:
- appendChild
- insertBefore
- removeChild
- replaceChild
原生没有提供insertAfter方法,可以使用appendChild和insertBefore两个API实现。这修改类型的API,全都是在父元素上操作的,比如我们要删除一个元素的上一个元素,就先用parentNode获取其父元素。
另外,Node还提供了一些高级用法:
- compareDocumentPosition 是以用于比较两个节点关系的函数。
- contains 检查一个节点是否包含另一个节点的函数
- isEqualNode 检查两个节点是否相同
- isSamaNode 检查两个节点是否为同一个节点,实际上在JavaScript 中可以用"==="
- cloneNode 复制一个节点,如果传入参数true,则会连同子元素做深拷贝。
DOM标准规定节点必须从文档的create方法创建出来,不能使用原生的JavaScript的new运算。于是document对象有这些方法:
- createElement
- createTextNode
- createCDATASection
- createComment
- createProcessingInstruction
- createDocumentFragment
- createDocumentType
Element 与 Attribute
Element表示元素,它是Node的元素。首先,我们可以把元素的Attribute当作字符串来看待,这样就有以下的API:
- getAttribute
- setAttribute
- removeAttribute
- hasAttribute
如果你追求极致的性能,还可以把Attribute当作节点:
- createElement
- createTextNode
查找元素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); // null
var winter = document.createElement('div');
winter.setAttribute('class', 'winter')
document.documentElement.appendChild(winter)
console.log(collection.length); // [Element]
遍历
前面已经提到过,通过Node的相关属性,我们可以用JavaScript遍历整个树。实际上,DOM API中还提供了NodeIterator 和 TreeWalker 来遍历树。
比起直接用属性遍历,NodeIterator 和 TreeWalker提供了过滤功能,还可以把属性节点也包含在遍历之内。
NodeIterator的基本用法示例如下:
var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMM
var node;
while(node = iterator.nextNode())
{
console.log(node);
}
这个API设计非常老派,原因主要有两点,一是循环没有类似的“hasNext”这样的方法,而是直接以nextNode返回空来标志结束,二是第二个参数是掩码,这两个设计都是传统C语言里比较常见的用法。
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);
}
比起NodeIterator,TreeWalker多了在DOM树上自由移动当前节点的能力,一般来说,这种API用于“跳过”某些节点,或者重复遍历某些节点。
总的来说,并不推荐使用这两个API,建议需要遍历DOM的时候,直接使用递归和Node的属性。
Range
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"))
此文章为7月Day30学习笔记,内容来源于极客时间《重学前端》,日拱一卒,每天进步一点点💪💪
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。