1. 程式人生 > 其它 >重學前端(9) 瀏覽器DOM:你知道HTML的節點有哪幾種嗎?

重學前端(9) 瀏覽器DOM:你知道HTML的節點有哪幾種嗎?

DOM,指的就是狹義的文件物件模型。 DOM API 介紹 首先我們先來講一講什麼叫做文件物件模型。   顧名思義,文件物件模型是用來描述文件,這裡的文件,是特指 HTML 文件(也用於 XML 文件,但是本課不討論 XML)。同時它又是一個“物件模型”,這意味著它使用的是物件這樣的概念來描述 HTML 文件。 說起 HTML 文件,這是大家最熟悉的東西了,我們都知道,HTML 文件是一個由標籤巢狀而成的樹形結構,因此,DOM 也是使用樹形的物件模型來描述一個HTML 文件。   DOM API 大致會包含 4 個部分。
  • 節點:DOM 樹形結構中的節點相關 API。
  • 事件:觸發和監聽事件相關 API。
  • Range:操作文字範圍相關 API。
  • 遍歷:遍歷 DOM 需要的 API。
  事件相關 API 和事件模型,我們會用單獨的課程講解,所以我們本篇文章重點會為你介紹節點和遍歷相關 API。   DOM API 數量很多,我希望給你提供一個理解 DOM API 設計的思路,避免單靠機械的方式去死記硬背。   節點 DOM 的樹形結構所有的節點有統一的介面 Node,我們按照繼承關係,給你介紹一下節點的型別。

 

在這些節點中,除了 Document 和 DocumentFrangment,都有與之對應的 HTML 寫法,我們可以看一下。

 

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 的作用。這幾個 API 的設計可以說是飽受詬病。其中最主要的批評是它不對稱——只有 before,沒有 after,而jQuery 等框架都對其做了補充。實際上,appendChild 和 insertBefore 的這個設計,是一個“最小原則”的設計,這兩個 API 是滿足插入任意位置的必要 API,而 insertAfter,則可以由這兩個API 實現出來。   所有這幾個修改型的 API,全都是在父元素上操作的,比如我們要想實現“刪除一個元素的上一個元素”,必須要先用parentNode 獲取其父元素。   這樣的設計是符合面向物件的基本原則的。還記得我們在 JavaScript 物件部分講的物件基本特徵嗎?“擁有哪些子元素”是父元素的一種狀態,所以修改狀態,應該是父元素的行為。這個設計我認為是 DOM API 中好的部分。 到此為止,Node 提供的 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 子類中,有一系列操作屬性的方法。   我們需要注意,對 DOM 而言,Attribute 和 Property 是完全不同的含義,只有特性場景下,兩者才會互相關聯(這裡在後面我會詳細講解,今天的文章裡我就不展開了)。   首先,我們可以把元素的 Attribute 當作字串來看待,這樣就有以下的 API:
  • getAttribute
  • setAttribute
  • removeAttribute
  • hasAttribute
如果你追求極致的效能,還可以把 Attribute 當作節點:
  • getAttributeNode
  • setAttributeNode
此外,如果你喜歡 property 一樣的訪問 attribute,還可以使用 attributes 物件,比如 document.body.attributes.class = “a” 等效於document.body.setAttribute(“class”, “a”)。   查詢元素   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);
var winter = document.createElement('div');
winter.setAttribute('class', 'winter')
document.documentElement.appendChild(winter)
console.log(collection.length);
  在這段程式碼中,我們先獲取了頁面的 className 為 winter 的元素集合,不出意外的話,應該是空。   我們通過 console.log 可以看到集合的大小為 0。之後我們添加了一個 class 為 winter 的 div,這時候我們再看集合,可以發現,集合中出現了新新增的元素。   這說明瀏覽器內部是有高速的索引機制,來動態更新這樣的集合的。所以,儘管 querySelector 系列的 API 非常強大,我們還是應該儘量使用 getElement 系列的API。   遍歷 前面已經提到過,通過 Node 的相關屬性,我們可以用 JavaScript 遍歷整個樹。實際上,DOM API 中還提供了 NodeIterator 和 TreeWalker 來遍歷樹。 比起直接用屬性來遍歷,NodeIterator 和 TreeWalker 提供了過濾功能,還可以把屬性節點也包含在遍歷之內。NodeIterator 的基本用法示例如下:
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 位運算並不高效,掩碼的設計就徒增複雜性了。   這裡請你注意一下這個例子中的處理方法,通常掩碼型引數,我們都是用按位或運算來疊加。而針對這種返回 null 表示結束的迭代器,我使用了在 while 迴圈條件中賦值,來保證迴圈次數和呼叫 next 次數嚴格一致(但這樣寫可能違反了某些編碼規範)。 我們再來看一下 TreeWalker 的用法。
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 用於“跳過”某些節點,或者重複遍歷某些節點。   總的來說,我個人不太喜歡 TreeWalker 和 NodeIterator 這兩個 API,建議需要遍歷 DOM 的時候,直接使用遞迴和 Node 的屬性。   Range   Range API 是一個比較專業的領域,如果不做富文字編輯類的業務,不需要太深入。這裡我們就僅介紹概念和給出基本用法的示例,你只要掌握即可。   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 spacerange.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"))

這個例子展示瞭如何使用 range 來取出元素和在特定位置新增新元素。

總結 DOM API 大致會包含 4 個部分。
  • 節點:DOM 樹形結構中的節點相關 API。
  • 事件:觸發和監聽事件相關 API。
  • Range:操作文字範圍相關 API。
  • 遍歷:遍歷 DOM 需要的 API。
DOM API 中還提供了 NodeIterator 和 TreeWalker 來遍歷樹。比起直接用屬性來遍歷,NodeIterator 和 TreeWalker 提供了過濾功能,還可以把屬性節點也包含在遍歷之內。   最後,請你用 DOM API 來實現遍歷整個 DOM 樹,把所有的元素的 tagName 打印出來。