天天看点

跟随 Web 标准探究DOM -- Node 与 Element 的遍历

写在前面

这篇没有什么 WebKit 代码的分析,因为……没啥好分析的,在实现里无非就是树的(先序DFS)遍历而已,囧哈哈哈……在

WebCore/dom/Node.h

WebCore/dom/ContainerNode.h

WebCore/dom/Element.h

以及对应的 .cpp 里看两眼就行了。下面这些属性一般都作为了私有变量直接放在了对象里(按照命名规范基本都叫

m_xxx

),然后通过和标准同名的 public 方法返回。不过要注意一下它们放在了哪里,比如

Node

里和子节点相关的方法一般定义到了 ContainerNode.h,

Node

里需要意识到

Element

存在的方法一般放去了 Element.h (即使定义时是

Node::xxx

这样的)。

这篇主要分析一下对作为 Node 的元素和作为 Element 的元素进行遍历的不同,以及总结一下各浏览器对这些 API 的兼容性。

Node

的遍历

Node

继承

EventTarget

Document

DocumentFragment

Element

Node

,所以下面提到的属性

Document

DocumentFragment

Element

都可以用。

Node.parentNode

标准

DOM 1定义在

Node

interface,原型

readonly attribute Node parentNode

,指明

Document

DocumentFragment

Attr

和不在树中的 node 的

parentNode

null

DOM 2,DOM 3,WHATWG,DOM 4 都和 DOM 1 一致

注意点

这是一个只读属性,所以不能通给一个元素的

parentNode

赋值来移动它,任何对这个引用的赋值操作都会被无视。比如:

node.parentNode = anotherNode;
console.log(node.parentNode === anotherNode); // false      

但是你可以修改它的

parentNode

的属性。

node.parentNode.title = "foo";
console.log(node.parentNode.title); // foo      

此外,

Document

Attr

没有

parentNode

还好理解,但是

Attr

没有就有点不好理解了,而且

Entity

Notation

也是没有的 —— 反向理解,

Node.childNodes

也是不算 attribute node,entity node 之类的,人家不把你当孩子,你也没必要把人家当父母。

没有 parent 的

Node

(比如刚刚用

createElement

创建或者用

removeChild

删除)的这个属性是 null。

兼容性

IE8- 里的

parentNode

有几个 bug:

新创建的元素的

parentNode

是 null,但修改过内容(比如用

innerHTML

或者

appendChild

)之后就会变成

DocumentFragment

var foo = document.createElement('div');
console.log(foo.parentNode);  // null
foo.innerHTML = "bar"
console.log(foo.parentNode);  // [object HTMLDocument]
console.log(foo.parentNode.nodeType);  // 11 = DocumentFragment      

从文档中删掉的节点,

parentNode

DocumentFragment

。对如下 HTML:

<div id="foo">
    <div id="bar"></div>
</div>      

执行 JS:

var foo = document.getElementById('bar');
console.log(foo.parentNode);  // [object HTMLDivElement]
foo.parentNode.removeChild(foo);
console.log(foo.parentNode);  // [object HTMLDocument]
console.log(foo.parentNode.nodeType);  // 11 = DocumentFragment      

Node.firstChild

Node.lastChild

DOM 1(

firstChild

lastChild

)定义在

Node

readonly attribute Node firstChild

readonly attribute Node lastChild

Document

DocumentFragment

Attr

parentNode

null

DOM 2(

firstChild

lastChild

),DOM 3(

firstChild

lastChild

), WHATWG (

firstChild

lastChild

),DOM 4(

firstChild

lastChild

) 和 DOM 1 一致

这是一个只读属性,和

parentNode

一样是不能重新赋值的。

注意浏览器可能(而且很多都)将 text node 和 comment node 算在一个 node 的 child nodes 里(HTML 文本里的缩进和断行都会算成新的 text node 夹杂在元素之间),并且

document.firstNode

可能是 doctype,因此不能判定

firstChild

返回的是一个元素,如果想得到第一个元素的话,需要手动检查

nodeType

并往后过滤。

CSS pseudo element 不会被算入。

W3C FAQ 解释了为什么有 DOM 的实现会将空白字符算作 text node:

DOM 必须将处理过的 XML (且为了方便,很多 DOM 的实现会将 XML 与 HTML 的许多处理合并)原文全部交给应用,空白字符也不能丢掉(这样 DOM 树与 XML 文本才能完成一一映射),那么就应该找个类型的 node 将它塞进去了 -- 最合适的就是 text node。

IE 8- 不将空白的 text node 算作子节点,IE 9+及其他浏览器都算。对如下HTML:

<div id="foo">

</div>      
var foo = document.getElementById('foo');
console.log(foo.firstChild);  // null in IE8-, supposed be a text node      

Node.nextSibling

Node.previousSibling

previousSibling

nextSibling

Node

readonly attribute Node previousSibling

readonly attribute Node nextSibling

,不存在对应 node 的返回

null

previousSibling

nextSibling

previousSibling

nextSibling

)和 DOM 1 一致。

WHATWG (

previousSibling

nextSibling

) 和 W3C DOM 一致,另外说明了 sibling 的概念 和 树中相对位置的概念(按照tree order,即先序DFS)

DOM 4(

previousSibling

nextSibling

)和 WHATWG 一致。

Node.firstChild

Node.lastChild

的注意事项类似。

IE 8- 不将空白的 text node 算作 sibling,IE 9+及其他浏览器都算。

HTML:

<div></div>  <div id="foo"></div>      

JS:

var foo = document.getElementById('foo');
// [object HTMLDivElement] in IE8-, supposed to be a text node
console.log(foo.previousSibling);      

Node.childNodes

Node

readonly attribute NodeList childNodes

,指明了返回的

NodeList

是 live 的,且如果没有子节点时返回空的

NodeList

.

DOM 2,DOM 3 和 DOM 1 一致。

WHATWG 原型

[SameObject] readonly attribute NodeList childNodes

,和 W3C DOM 一致。DOM 4 和 WHATWG 一样。

Node.firstChild

Node.lastChild

的注意事项类似。返回的

NodeList

元素是只读的(可以改元素属性,不可以改引用)。要增删子元素的话对

childNodes

动脑筋是没用的……(注意:其他浏览器对

childNodes

中引用的修改仅仅是无视,但 IE 会怒报错)

<div id="foo"><p></p></div>      

JS:

var foo = document.getElementById('foo');
console.log(foo.childNodes.length);  // 1

var bar = document.createElement('div');

foo.childNodes[0] = bar;  // attempt to replace a child, throws error in IE
console.log(foo.childNodes[0].nodeName);  // "P", not replaced

foo.childNodes[1] = bar;  // attempt to add a child, throws error in IE
console.log(foo.childNodes.length);  // 1, not added

delete foo.childNodes[0];  // attempt to delete a child, throws error in IE
console.log(foo.childNodes.length);  // 1, not deleted      

一般

document.childNodes

只有 doctype 和

<html>

元素,除非原文两者之间有注释。

元素的排列顺序是 document order,即按照 DOM 树中的先序 DFS 排列。

IE 8- 不将空白的 text node 算作子节点,IE 9+及其他浏览器都算。

<div id="foo">   </div>      
var foo = document.getElementById('foo');
// 0 in IE8-, supposed to be 1
console.log(foo.childNodes.length);      

Element

Element

Node

的区别在于

Element

不包括 text node,comment node,etc. 实际上,

Element

继承自

Node

,也就是说它本来就是

Node

的一种。

Element

都具备(或者说,应该具备)

Node.nodeType == Node.ELEMENT_NODE

这个特性(还有其他哪几种

nodeType

参阅WHATWG标准,这里先不展开叙述)。以下的几种 API 可以看成

Node

版的 API 加上对结果进行

Node.nodeType == Node.ELEMENT_NODE

过滤(实际上 WebKit 的实现也基本都是这样干的)。

注意作为

Element

的遍历 API 基本都属于 HTML5 的新特性,W3C 标准里一般都只能在 DOM 4 里找到。

Node.parentElement

WHATWG 将

parentElement

定义在了 Node ,原型

readonly attribute Element? parentElement

。W3C DOM 4 也一样。

乍一看,定义在

Node

似乎有点怪,不过仔细一想其实是很合理的 ——

Element

的子节点不一定是

Element

,譬如 text node。你不能阻碍人家寻亲的能力啊 :D

如果

Node

的父元素不是

Element

,返回的是 null。

实际上

parentElement

一开始是 IE 特有的(起码从 IE6 开始就有了),但 IE 仅为

Element

定义了这个属性(即是说 text node 之类的是不能用的)。此后这个属性进入了标准,目前基本各大浏览器都支持它,主要的兼容性问题出现在 IE 不支持非

Element

Node

使用这个属性。如果仅对

Element

使用它的话,是可以放心用的。

此外由于 IE8- 中

parentNode

有不轻的 bug(见前文),在只需要

Element

的场景下,可能用

parentElement

是更好的选择。

ParentNode.firstElementChild

ParentNode.lastElementChild

目前 WHATWG 将

firstElementChild

lastElementChild

定义在了

ParentNode

,原型为

readonly attribute Element? firstElementChild;
readonly attribute Element? lastElementChild;      

它们原本在

ElementTraversal

,后来为了降低耦合,WHATWG 将

ElementTraversal

按照功能分割成了两个 interface

ParentNode

ChildNode

,而

firstElementChild

lastElementChild

自然就挪去了针对有子元素的

Node

设置的

ParentNode

目前继承

ParentNode

的包括

Document

Element

DocumentFragment

,所以这三个 interface 的对象是可以访问

firstElementChild

lastElementChild

的。

W3C DOM4 和 WHATWG 一致,但是注意 DOM4 目前还不是 W3C Recommendation。目前处于 W3C Recommendation 状态的标准里,

firstElementChild

lastElementChild

仍然定义在

ElementTraversal

。按照 Element Traversal 标准的规定,所有的

Element

都必须实现

ElementTraversal

,但对其他 interface 不作要求。

因此,这两个属性在 WHATWG 和 W3C 的标准里存在分歧:WHATWG 标准中,

Document

Element

DocumentFragment

均有这两个属性;W3C 标准中,目前仅有

Element

具有这两个属性。但因为和 WHATWG 一致的 DOM4 将来很有可能成为 W3C Recommendation,W3C 标准最后很有可能会和 WHATWG 一样,三种对象均有这两个属性。

如果没有子元素,返回的是 null。这两个属性也是只读的,可以在子元素上修改它的属性,但不可更改引用(会被无视)。

由于属于较新的 API,在

Element

上的使用要 IE 9+ 才支持,其他浏览器的现行版本都有支持。

因为在 WHATWG 和 W3C 的现行标准里存在分歧,

Document

DocumentFragment

对这两个属性的支持在各浏览器中不太一致。偏 WHATWG 的 Chrome,Firefox 和 Opera 支持

Document

Element

DocumentFragment

,IE 9+ 和 Safari 仅支持

Element

。考虑到 DOM4 将来应该会成为 W3C Recommendation,最后应该是三个 interface 都能支持的(当然,IE 就不能指望旧版本支持了……)

NonDocumentTypeChildNode.nextElementSibling

NonDocumentTypeChildNode.previousElementSibling

在 WHATWG 标准里,和为了照顾 jQuery 兼容性而为

getElementById

专门设一个

NonElementParentNode

(而不是

ParentNode

)类似,为了照顾现存网页的兼容性,

nextElementSibling

previousElementSibling

被定义在了一个专门分出来的

NonDocumentTypeChildNode

ChildNode

)里,参见 bug tracker上的讨论。

目前

NonDocumentTypeChildNode

的定义如下:

[NoInterfaceObject]
interface NonDocumentTypeChildNode {
  readonly attribute Element? previousElementSibling;
  readonly attribute Element? nextElementSibling;
};
Element implements NonDocumentTypeChildNode;
CharacterData implements NonDocumentTypeChildNode;      

注:目前 WHATWG 标准里

ParentNode

NonElementParentNode

ChildNode

NonDocumentTypeChildNode

之间的关系如下图:

跟随 Web 标准探究DOM -- Node 与 Element 的遍历

W3C DOM4 与 WHATWG 一致,但与

ParentNode.firstElementChild

ParentNode.lastElementChild

的情况类似的是,按照目前处于 W3C Recommendation 的 Element Traversal 的定义,只有

Element

拥有这两个属性,

CharacterData

没有。

类似

ParentNode.firstElementChild

ParentNode.lastElementChild

也与

ParentNode.firstElementChild

ParentNode.lastElementChild

类似,需要 IE9+。Chrome,Firefox 和 Opera 支持

Element

CharacterData

上访问这两个属性,IE 9+ 和 Safari 仅支持

Element

, 如果 W3C DOM 4 进入 Recommendation,很可能会统一。

ParentNode.childElementCount

WHATWG / DOM4 定义在

ParentNode

,原型

readonly attribute unsigned long childElementCount

。W3C Recommendation 里目前定义在

ElementTraversal

,原型和 WHATWG 一样。

在符合标准的实现里,约等于

container.children.length

ParentNode.firstElementChild

的情况类似,需要 IE9+,Chrome,Firefox 和 Opera 支持

Document

Element

DocumentFragment

Element

ParentNode.children

虽然这个 API 很早就存在,但直到最近才标准化。WHATWG / DOM4 定义在

ParentNode

[SameObject] readonly attribute HTMLCollection children

,指明是一个 live 的

HTMLCollection

而不是

NodeList

,也就是说元素必然全是

Element

(历史遗留问题带来的囧命名,和

Node

那边的名字对不上号,不叫

childElements

而叫

children

,不叫

ElementList

HTMLCollection

……)。

Node.childNodes

,得到的

HTMLCollection

是 live 且(引用)只读的。

该属性最早出现在 IE 中,IE6 开始具备这个属性。此后各大浏览器跟着实现,Firefox是最后一个实现这个属性的主要浏览器(3.5开始,也蛮久了)。但是由于 WHATWG 标准的接受度不同,Chrome,Firefox 和 Opera 在支持

Document

Element

DocumentFragment

上使用该属性,IE 和 Safari 仅支持

Element

。 Chrome 和 Firefox 还实验性地支持在

SVGElement

上使用该属性。

另外,IE8- 的

children

会包含 comment node。

<div id="foo"><!-- comment --></div>      
var foo = document.getElementById('foo');
console.log(foo.children.length);  // 1, supposed to be 0      

继续阅读