Javascript 包括三塊:ECMAScript、DOM 和 BOM,本文主要介紹 DOM。
# DOM
# 什么是 DOM
文檔對象模型 (DOM
) 是HTML
和XML
文檔的編程接口。文檔對象模型(Document Object Model)
DOM
將文檔解析為一個由節點和對象(包含屬性和方法的對象)組成的結構集合。
盡管通常會使用JavaScript
來訪問DOM
, 但它並不是JavaScript
的一部分,它也可以被其他語言使用。
# DOM 解析
我們常見的HTML
元素,在瀏覽器中會被解析成節點:
在控制台,我們也能比較清晰地看到這樣的層級關系:
節點樹中的節點彼此擁有層級關系。
父(parent
)、子(child
)和同胞(sibling
)等術語用於描述這些關系。父節點擁有子節點。同級的子節點被稱為同胞(兄弟或姐妹)。
- 在節點樹中,頂端節點被稱為根(
root
) - 每個節點都有父節點、除了根(它沒有父節點)
- 一個節點可擁有任意數量的子
- 同胞是擁有相同父節點的節點
通過HTML DOM
,樹中的所有節點均可通過JavaScript
進行訪問。所有HTML
元素(節點)均可被修改,也可以創建或刪除節點。
# DOM 接口
DOM
接口主要用於操作DOM
節點,如常見的增刪查改。
在 web 和 XML 頁面腳本中使用 DOM 時,一些常用的 API 如下:
document.getElementById(id)
:根據id
獲取元素document.getElementsByTagName(name)
:根據tag
獲取元素document.createElement(name)
:創建元素parentNode.appendChild(node)
:添加子元素element.innerHTML
:設置/獲取元素內容element.styles
:設置/獲取元素樣式element.setAttribute()
:設置元素屬性值element.getAttribute()
:獲取元素屬性值element.addEventListener()
:添加事件綁定
通常什么時候會用呢,最常見的便是列表的維護,包括增加新的選項、刪除某個、修改某個等等。
在瀏覽器兼容性問題很多的時候,我們常常會使用jQuery
來進行些DOM
操作,如今兼容性問題逐漸變少,大家更傾向於用原生DOM
接口來進行操作。
# DOM 事件流
事件流所描述的就是從頁面中接受事件的順序。
DOM 事件流(event flow
)存在三個階段:事件捕獲階段、處於目標階段、事件冒泡階段。
- 捕獲階段:一開始從文檔的根節點流向目標對象;
- 目標階段:然后在目標對向上被觸發;
- 冒泡階段:之后再回溯到文檔的根節點。
# 事件捕獲
當鼠標點擊或者觸發 dom 事件時,瀏覽器會從根節點開始由外到內進行事件傳播,即點擊了子元素,如果父元素通過事件捕獲方式注冊了對應的事件的話,會先觸發父元素綁定的事件。
在事件捕獲的概念下在p
元素上發生click
事件的順序應該是document -> html -> body -> div -> p
。
# 事件冒泡
與事件捕獲恰恰相反,事件冒泡順序是由內到外進行事件傳播,直到根節點。
在事件冒泡的概念下在p
元素上發生click
事件的順序應該是p -> div -> body -> html -> document
。
DOM
標准事件流的觸發的先后順序為:先捕獲再冒泡,即當觸發 dom 事件時,會先進行事件捕獲,捕獲到事件源之后通過事件傳播進行事件冒泡。
不同的瀏覽器對此有着不同的實現,IE10 及以下不支持捕獲型事件,所以就少了一個事件捕獲階段,IE11、Chrome 、Firefox、Safari 等瀏覽器則同時存在。
曾經踩過 IE9 中button
的坑,例如<button><span></span></button>
,如果我們分別在button
以及span
里均綁定click
事件,則span
的事件不會被觸發。不知道這個跟事件機制是否相關呢?
addEventListener addEventListener
的第三個參數就是為冒泡和捕獲准備的. addEventListener
有三個參數:
element.addEventListener(event, function, useCapture)
event
:需要綁定的事件function
:觸發事件后要執行的函數useCapture
:默認值是 false,表示在事件冒泡階段調用事件處理函數。如果參數為 true,則表示在事件捕獲階段調用處理函數。
# 事件委托
基於事件冒泡機制,我們可以實現將子元素的事件委托給父級元素來進行處理。
當我們需要對很多元素添加事件的時候,可以通過將事件添加到它們的父節點而將事件委托給父節點來觸發處理函數。
這樣能解決什么問題呢?
- 綁定子元素會綁定很多次的綁定,而綁定父元素只需要一次綁定。
- 將事件委托給父節點,這樣我們對子元素的增加和刪除、移動等,都不需要重新進行事件綁定。
很常見的就是我們有個列表,每個選項都可以進行編輯、刪除、添加標簽等功能,而把事件委托給父元素或者document
,不管我們新增、刪除、更新選項,都不需手動去綁定和移除事件。
最常在jQuery
中使用事件委托:
$("#my-list").delegate("button", "click", function() { // "$(this)"是被click的元素 console.log("you clicked a button", $(this)); });
現在我們基本上都使用框架了,我們可以隨意地在元素上綁定事件,如 Vue 中<div @click="myClickEvent" />
,因為框架會幫我們用事件委托的方式處理掉,大部分都會綁定在最外層初始化的id
元素,或者是document
吧。
# 虛擬 DOM
一個DOM
節點元素,其實是很復雜的,包含了很多的屬性和方法。
我們來簡單打印一下一個DOM
元素:
看到右邊的滾動條了沒,有如此之多的屬性。
所以隨着應用程序越來越復雜,DOM
操作越來越頻繁,需要監聽事件和在事件回調用更新頁面的 DOM 操作也越來越多,性能消耗則會比較大。於是乎,虛擬DOM
的想法便被人提出並實現了。
虛擬DOM
其實是用來模擬真實DOM
的中間產物,主要包括以下功能:
1. 用JS
對象模擬DOM
樹,簡化DOM
對象。
簡單來說,就是用一個對象模擬,保留主要的一些DOM
屬性,其他的則去掉。
2. 使用虛擬DOM
,結合操作DOM
的接口,來生成真實DOM
。
使用假DOM
生成真DOM
,同時保持真實DOM
對象的引用,以便 3 步驟的執行。
3. 更新DOM
時,比較兩棵虛擬DOM
樹的差異,局部更新真實DOM
。
這個就比較有意思,可以根據數據的變化,來最小化地移動、替換、刪除原有的DOM
元素。
結合使用以上功能,便能在復雜應用中更好地維護了。而我們現在很多的前端框架,例如 Angular、React、Vue 等,都為了給開發者提供便捷的數據綁定機制、高效的 DOM 更新機制而做了不少的工作,更多的可以參考《如何理解前端和 Vue》一文。