你知道HTML的节点有哪几类吗

浏览器在前端开发中无时无刻不在接触,但是浏览器里面的DOM对象及其DOM API真正的了解吗?
DOM API 是最早被设计出来的一批 API,也是用途最广的一批 API,而这里要学习的 DOM,指的是狭义的文档对象模型。

DOM API 介绍

在学习 DOM API 前先了解一下文档对象模型,顾名思义,文档对象模型是用来描述文档,这里的文档特指的是 HTML 文档(也用于 XML 文档);同时,它又是一个“对象模型”,这意味着它使用的是对象这样的概念来描述 HTML 文档。
而 HTML 文档是一个由标签嵌套而成的树形结构,因此,DOM 也是使用树形的对象模型来描述一个 HTML 文档。
DOM API 大致包含四个部分:

  • 节点:DOM 树形结构中的节点相关 API
  • 事件:触发和监听事件相关 API
  • Range:操作文字范围相关 API
  • 遍历:遍历 DOM 需要的相关 API

下面对节点进行学习。

节点

DOM 的所有树形结构所有的节点有统一的接口 Node,按照继承关系如下图(来自winter老师):

在这些节点中,除了 Document 和 DocumentFragment 都有与之对应的 HTML 写法。

1
2
3
4
5
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 也是见名知意,但它们受到的最主要批评是不对称 —— 只有 before,没有 after,而 jQuery 框架对其进行了补充。
实际上,appendChild 和 insertBefore 的这个设计是一个“最小原则”的设计,这两个 API 是满足插入任意位置的必要 API,如果要实现 insertAfter 这样的 API,则可以由这两个 API 来实现。
还有一点,这几个修改型的 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 子类中,有一系列操作属性的方法。
首先,把元素的 Attribute 当做字符串看待,有如下 API:

  • getAttribute
  • setAttribute
  • removeAttribute
  • hasAttribute

如果追求性能,还可以吧 Attribute 当做节点:

  • getAttributeNode
  • setAttributeNode

此外,还可以使用 attributes 对象,如:
document.body.attributes.class = ‘class名称’ 等效于 document.body.setAttribute(‘class’, ‘class名称’)。

查找元素

document 节点提供了查找元素的能力,有如下 API:

  • querySelector
  • querySelectorAll
  • getElementById
  • getELementsByName
  • getELementsByTagName
  • getElementsByClassName

需要知道的是,getElementById、getElementsByName、getElementsByTagName、getElementsByClassName 这几个 API 性能高于 querySelector;而这几个 API 获取的集合并非数组,而是一个能够动态更新的集合。

遍历

通过 Node 的相关属性,我们可以用 JavaScript 遍历整个树,实际上,DOM API 还提供了 NodeIterator 和 TreeWalker 来遍历树。
比起直接用属性来遍历,NodeIterator 和 TreeWalker 提供了过滤功能(来自winter老师课程),还可以把属性节点也包含在遍历之内;NodeIterator 基本用法:

1
2
3
4
5
var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);
var node;
while(node = iterator.nextNode()) {
console.log(node);
}

再看一下 TreeWalker 的基本用法:

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

这里两个方法一般情况不会怎么使用,这里了解一波。

Range

Range API 是一个更加专业的领域,它主要用来做富文本编辑之类的业务功能,这里仅做学习理解。Range API 表示一个 HTML 上的范围,这个范围是以文字为最小单位的,所以 Range 不一定包含完整的节点,它可能是 Text 节点中的一段等。
通过 Range API 能够比节点 API 更准确的操作 DOM 树,凡是节点 API 能做到的,Range API 都可以做到,而且性能更好,一般在实际项目中不适用,只有底层框架和做富文本编辑器有强需求。
创建 Range 一般是设置它的起止来实现,看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var range = new Range(),
firstText = p.childNodes[1],
secondText = em.firstChild;
range.setStart(firstText, 9);
range.setEnd(secondText, 4);

// 处理用户选中区域
var range = document.getSelection().getRangeAt(0);

// 更改 Range 选中区段内容的方式主要是取出和插入
var fragment = range.extractContents();
range.insertNode(document.createTextNode("aaaa"));

总结

从 DOM API 的内容及分类进行了学习,对它相应的一些操作 API 进行了补强。