跟隨 Web 標准探究DOM -- Node 與 Element 的遍歷


寫在前面

這篇沒有什么 WebKit 代碼的分析,因為……沒啥好分析的,在實現里無非就是樹的(先序DFS)遍歷而已,囧哈哈哈……在WebCore/dom/Node.hWebCore/dom/ContainerNode.hWebCore/dom/Element.h 以及對應的 .cpp 里看兩眼就行了。下面這些屬性一般都作為了私有變量直接放在了對象里(按照命名規范基本都叫m_xxx),然后通過和標准同名的 public 方法返回。不過要注意一下它們放在了哪里,比如Node里和子節點相關的方法一般定義到了 ContainerNode.h,Node 里需要意識到 Element 存在的方法一般放去了 Element.h (即使定義時是Node::xxx 這樣的)。

這篇主要分析一下對作為 Node 的元素和作為 Element 的元素進行遍歷的不同,以及總結一下各瀏覽器對這些 API 的兼容性。

Node 的遍歷

Node 繼承 EventTargetDocumentDocumentFragmentElement繼承Node,所以下面提到的屬性DocumentDocumentFragmentElement都可以用。


Node.parentNode

標准

DOM 1定義在 Node interface,原型readonly attribute Node parentNode,指明DocumentDocumentFragmentAttr 和不在樹中的 node 的 parentNodenull

DOM 2DOM 3WHATWGDOM 4 都和 DOM 1 一致

注意點

這是一個只讀屬性,所以不能通給一個元素的parentNode賦值來移動它,任何對這個引用的賦值操作都會被無視。比如:

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

但是你可以修改它的parentNode的屬性。

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

此外,DocumentAttr 沒有parentNode還好理解,但是 Attr 沒有就有點不好理解了,而且EntityNotation也是沒有的 —— 反向理解,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

從文檔中刪掉的節點,parentNodeDocumentFragment。對如下 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.firstChildNode.lastChild

標准

DOM 1(firstChildlastChild)定義在 Node interface,原型readonly attribute Node firstChildreadonly attribute Node lastChild,指明DocumentDocumentFragmentAttr 和不在樹中的 node 的 parentNodenull

DOM 2(firstChildlastChild),DOM 3(firstChildlastChild), WHATWG (firstChildlastChild),DOM 4(firstChildlastChild) 和 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>

執行 JS:

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

Node.nextSiblingNode.previousSibling

標准

DOM 1(previousSiblingnextSibling)定義在 Node interface,原型readonly attribute Node previousSiblingreadonly attribute Node nextSibling,不存在對應 node 的返回 null

DOM 2(previousSiblingnextSibling),DOM 3(previousSiblingnextSibling)和 DOM 1 一致。

WHATWG (previousSiblingnextSibling) 和 W3C DOM 一致,另外說明了 sibling 的概念樹中相對位置的概念(按照tree order,即先序DFS)

DOM 4(previousSiblingnextSibling)和 WHATWG 一致。

注意點

Node.firstChildNode.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

標准

DOM 1定義在 Node interface,原型readonly attribute NodeList childNodes,指明了返回的 NodeList 是 live 的,且如果沒有子節點時返回空的 NodeList.

DOM 2DOM 3 和 DOM 1 一致。

WHATWG 原型 [SameObject] readonly attribute NodeList childNodes,和 W3C DOM 一致。DOM 4 和 WHATWG 一樣。

注意點

Node.firstChildNode.lastChild的注意事項類似。返回的NodeList元素是只讀的(可以改元素屬性,不可以改引用)。要增刪子元素的話對childNodes動腦筋是沒用的……(注意:其他瀏覽器對childNodes中引用的修改僅僅是無視,但 IE 會怒報錯)

HTML:

<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+及其他瀏覽器都算。

HTML:

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

JS:

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

Element 的遍歷

ElementNode 的區別在於 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? parentElementW3C DOM 4 也一樣。

乍一看,定義在Node似乎有點怪,不過仔細一想其實是很合理的 —— Element 的子節點不一定是 Element,譬如 text node。你不能阻礙人家尋親的能力啊 :D

注意點

如果 Node 的父元素不是 Element,返回的是 null。

兼容性

實際上 parentElement 一開始是 IE 特有的(起碼從 IE6 開始就有了),但 IE 僅為 Element 定義了這個屬性(即是說 text node 之類的是不能用的)。此后這個屬性進入了標准,目前基本各大瀏覽器都支持它,主要的兼容性問題出現在 IE 不支持非 ElementNode 使用這個屬性。如果僅對 Element 使用它的話,是可以放心用的。

此外由於 IE8- 中 parentNode 有不輕的 bug(見前文),在只需要 Element 的場景下,可能用 parentElement 是更好的選擇。


ParentNode.firstElementChildParentNode.lastElementChild

標准

目前 WHATWG 將 firstElementChildlastElementChild 定義在了 ParentNode,原型為

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

它們原本在ElementTraversal,后來為了降低耦合,WHATWG 將 ElementTraversal 按照功能分割成了兩個 interface ParentNodeChildNode,而 firstElementChildlastElementChild自然就挪去了針對有子元素的Node設置的ParentNode

目前繼承 ParentNode 的包括DocumentElementDocumentFragment,所以這三個 interface 的對象是可以訪問firstElementChildlastElementChild的。

W3C DOM4 和 WHATWG 一致,但是注意 DOM4 目前還不是 W3C Recommendation。目前處於 W3C Recommendation 狀態的標准里, firstElementChildlastElementChild仍然定義在 ElementTraversal。按照 Element Traversal 標准的規定,所有的 Element 都必須實現 ElementTraversal,但對其他 interface 不作要求。

因此,這兩個屬性在 WHATWG 和 W3C 的標准里存在分歧:WHATWG 標准中,DocumentElementDocumentFragment 均有這兩個屬性;W3C 標准中,目前僅有 Element 具有這兩個屬性。但因為和 WHATWG 一致的 DOM4 將來很有可能成為 W3C Recommendation,W3C 標准最后很有可能會和 WHATWG 一樣,三種對象均有這兩個屬性。

注意點

如果沒有子元素,返回的是 null。這兩個屬性也是只讀的,可以在子元素上修改它的屬性,但不可更改引用(會被無視)。

兼容性

由於屬於較新的 API,在Element上的使用要 IE 9+ 才支持,其他瀏覽器的現行版本都有支持。

因為在 WHATWG 和 W3C 的現行標准里存在分歧,DocumentDocumentFragment 對這兩個屬性的支持在各瀏覽器中不太一致。偏 WHATWG 的 Chrome,Firefox 和 Opera 支持 DocumentElementDocumentFragment,IE 9+ 和 Safari 僅支持 Element。考慮到 DOM4 將來應該會成為 W3C Recommendation,最后應該是三個 interface 都能支持的(當然,IE 就不能指望舊版本支持了……)


NonDocumentTypeChildNode.nextElementSiblingNonDocumentTypeChildNode.previousElementSibling

標准

在 WHATWG 標准里,和為了照顧 jQuery 兼容性而為getElementById 專門設一個 NonElementParentNode (而不是ParentNode)類似,為了照顧現存網頁的兼容性,nextElementSiblingpreviousElementSibling 被定義在了一個專門分出來的 NonDocumentTypeChildNode(而不是ChildNode)里,參見 bug tracker上的討論

目前 NonDocumentTypeChildNode 的定義如下:

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

注:目前 WHATWG 標准里 ParentNodeNonElementParentNodeChildNodeNonDocumentTypeChildNode 之間的關系如下圖:

W3C DOM4 與 WHATWG 一致,但與ParentNode.firstElementChildParentNode.lastElementChild的情況類似的是,按照目前處於 W3C Recommendation 的 Element Traversal 的定義,只有 Element 擁有這兩個屬性,CharacterData 沒有。

注意點

類似 ParentNode.firstElementChildParentNode.lastElementChild

兼容性

也與 ParentNode.firstElementChildParentNode.lastElementChild類似,需要 IE9+。Chrome,Firefox 和 Opera 支持 ElementCharacterData上訪問這兩個屬性,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 支持 DocumentElementDocumentFragment,IE 9+ 和 Safari 僅支持 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 在支持 DocumentElementDocumentFragment上使用該屬性,IE 和 Safari 僅支持 Element。 Chrome 和 Firefox 還實驗性地支持在 SVGElement 上使用該屬性。

另外,IE8- 的 children 會包含 comment node。

HTML:

<div id="foo"><!-- comment --></div>

JS:

var foo = document.getElementById('foo');
console.log(foo.children.length);  // 1, supposed to be 0

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM