重學前端(9) 瀏覽器DOM:你知道HTML的節點有哪幾種嗎?
阿新 • • 發佈:2022-04-13
DOM,指的就是狹義的文件物件模型。
DOM API 介紹
首先我們先來講一講什麼叫做文件物件模型。
顧名思義,文件物件模型是用來描述文件,這裡的文件,是特指 HTML 文件(也用於 XML 文件,但是本課不討論 XML)。同時它又是一個“物件模型”,這意味著它使用的是物件這樣的概念來描述 HTML 文件。
說起 HTML 文件,這是大家最熟悉的東西了,我們都知道,HTML 文件是一個由標籤巢狀而成的樹形結構,因此,DOM 也是使用樹形的物件模型來描述一個HTML 文件。
DOM API 大致會包含 4 個部分。
Node 是 DOM 樹繼承關係的根節點,它定義了 DOM 節點在 DOM 樹上的操作,首先,Node 提供了一組屬性,來表示它在 DOM 樹中的關係,它們是:
- 節點:DOM 樹形結構中的節點相關 API。
- 事件:觸發和監聽事件相關 API。
- Range:操作文字範圍相關 API。
- 遍歷:遍歷 DOM 需要的 API。
在這些節點中,除了 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
- parentNode
- childNodes
- firstChild
- lastChild
- nextSibling
- previousSibling
- appendChild
- insertBefore
- removeChild
- replaceChild
- compareDocumentPosition 是一個用於比較兩個節點中關係的函式。
- contains 檢查一個節點是否包含另一個節點的函式。
- isEqualNode 檢查兩個節點是否完全相同。
- isSameNode 檢查兩個節點是否是同一個節點,實際上在 JavaScript 中可以用“===”。
- cloneNode 複製一個節點,如果傳入引數 true,則會連同子元素做深拷貝。
- createElement
- createTextNode
- createCDATASection
- createComment
- createProcessingInstruction
- createDocumentFragment
- createDocumentType
- getAttribute
- setAttribute
- removeAttribute
- hasAttribute
- getAttributeNode
- setAttributeNode
- querySelector
- querySelectorAll
- getElementById
- 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。