+
+

浅谈文档对象模型DOM

DOM,全称是文档对象模型Document Object Model,是一种使用“对象”概念用来描述HTML“文档”的“模型”,DOM的本质是一种Web API,Web API能将Web页面和编程语言(通常是JavaScript)连接起来。

我们知道,HTML文档可以看作是一个由标签嵌套而成的树形结构,而DOM使用的对象模型同样也可以理解为树型的。

DOM API大致包含4个部分:

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

节点 Node

DOM模型用一颗逻辑树来表示一个HTML文档,数的每个分支的终点都是一个节点,每个节点都包含着对象。我们就可以通过JavaScript等语言去操作这个模型,比如改变文档的结构、样式或是内容。

-w710

Node本质是一个接口,许多DOM API对象的类型会从这个接口继承。

-w847

除了DocumentDocumentFragement外,所有节点都在HTML中有具象的表示_Element就是<tagname>...</tagname>中的标签,Comment<!---->中的注释……

既然是API,自然就具备「属性」和「方法」了。属性用来获取节点的信息,通常是只读的;方法则是用来操作节点,有时也能获取节点信息。

常用的Node属性:

  • parentNode
  • childNodes
  • firstChild
  • lastChild
  • nextSibling
  • previousSibling

常用的Node方法:

  • appendChild
  • insertBefore
  • removeChild
  • replaceChild

从这些属性和方法的命名中,可以很容易理解它们的作用,就不再赘述,详细的说明可以查看Node - Web API 接口参考 | MDN

值得注意的是,上述所有的方法,都是针对父节点进行操作。因此很多时候要先用parentNode获取父节点再应用方法。这样的设计其实是符合面向对象的原则的,因为“拥有哪些子节点”是父节点的一种状态,所以修改子节点即修改状态,应当是归属于父节点的行为。

Document 文档根节点

继承自Node的Document,具备Node的所有属性和方法。由于DOM标准规定节点不能使用JavaScript原生的new方法,必须从Document对象中create出来,因此在创建节点时,需要用到如下方法:

  • createElement
  • createTextNode
  • createCDATASection
  • createComment
  • createProcessingInstruction
  • createDocumentFragment
  • createDocumentType

Document也提供了查找Element的能力:

  • querySelector
  • querySelectorAll
  • getElementById
  • getElementsByName
  • getElementsByTagName
  • getElementsByClassName

其中,getElementsBy系列方法的性能要优于querySelector,且它们获取到的子集是一个能够动态更新的集合。

不妨做个小实验验证:

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

//0
//1

这说明,浏览器内部有高速索引机制来动态更新由getElementsBy获取的集合。

Element 元素型节点

如前文所述,Element对应着HTML中的标签,所有Document对象下的对象都继承自它。

需要区分的是,由于直接与HTML代码相关,Element有两种属性,一种是HTML标签上的属性Attribute,比如常见的id="...."class="...",它的值只可能是字符串;另一种是前面说的DOM属性Property

由于具有两种属性,所以就会出现获取属性Attribute相关的属性Property

  • attributes
  • getAttribute
  • setAttribute
  • removeAttribute
  • hasAttribute

遍历

其实,通过Node的属性,就能够实现DOM树的遍历,不过DOM API还额外提供了NodeIteratorTreeWalker两种遍历方法,具有过滤功能,也能把属性节点包含在遍历范围内。

NodeIterator

1
2
3
4
5
6
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 位运算并不高效,掩码的设计就徒增复杂性了。

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);
}

比起 NodeIteratorTreeWalker 多了在 DOM 树上自由移动当前节点的能力,一般来说,这种 API 用于“跳过”某些节点,或者重复遍历某些节点。

总的来说,NodeIteratorTreeWalker的设计太过老派,如果只是简单的遍历,直接使用Node的属性反倒更加方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function DOMbfs(element, callback) { //广度优先遍历
var queue = []; //存放子节点的队列
while(element) {
callback(element);
if(element.children.length !== 0) {
for (var i = 0; i < element.children.length; i++) {
queue.push(element.children[i]);//存放子节点
}
}
element = queue.shift(); //取出第一项
}
}

DOMbfs(document, function(element){
console.log(element)
})

Range

Range API 表示一个 HTML 上的范围,这个范围是以文字为最小单位的,所以 Range 不一定包含完整的节点,它可能是 Text 节点中的一段,也可以是头尾两个 Text 的一部分加上中间的元素。

我们通过 Range API 可以比节点 API 更精确地操作 DOM 树,凡是 节点 API 能做到的,Range API 都可以做到,而且可以做到更高性能,但是 Range API 使用起来比较麻烦,所以在实际项目中,并不常用,只有做底层框架和富文本编辑对它有强需求。

创建Range一般通过设置它的起止实现:

1
2
3
4
5
6
7
8
9
10
const paragraphs = document.querySelectorAll('p');

// 创建 Range 对象
const range = new Range();

// Range 起始位置在段落2
range.setStartBefore(paragraphs[1]);

// Range 结束位置在段落3
range.setEndAfter(paragraphs[2]);

此外,通过Range也可以从光标选中区域创建:

1
var range = document.getSelection().getRangeAt(0);

借助于这个特性,可以更改选中区域的内容:

1
2
var fragment = range.extractContents()
range.insertNode(document.createTextNode("aaaa"))

参考

浏览器DOM:你知道HTML的节点有哪几种吗?
为初学者准备的:DOM速成
Node - Web API 接口参考 | MDN

本文作者: rhinoc

本文链接: https://www.rhinoc.top/cid154_1/

版权声明: 本博客所有文章除特别声明外,均采用BY-NC-SA 4.0国际许可协议,转载请注明。

打赏
Love U 3000
  • Through WeChat
  • Through Alipay
0%