虛擬 DOM 與 DOM Diff
虛擬 DOM 與 DOM Diff
本文寫於 2020 年 9 月 12 日
虛擬 DOM 在今天已經是前端離不開的東西了,因為他的好處實在是太多了。
在《高效能 JavaScript》一書中,提到過 DOM 操作很慢。但實際上這句話沒有任何前提條件,也沒有對比誰慢,純粹屬於“話術”。
的確,DOM 操作比 JS 原生 API 是要慢很多的,因為 DOM 操作是跨執行緒的。
但是並沒有我們想象的那麼慢。 從使用上來說,你寫網頁不可能不操作 DOM,不操作 DOM 就沒辦法寫各種效果了,就算是現在的 Vue/React 不用操作 DOM,但底層也是 Vue/React 幫我們操作了 DOM。
但是 Vue/React 採用的是虛擬 DOM,而不是普通的操作 DOM。虛擬 DOM 相對於 DOM 而言,效率更好、速度更快。
虛擬 DOM 的優點
減少 DOM 操作
剛剛我們說過了,DOM 操作是跨執行緒的,效能並不好,但寫網頁又不得不操作 DOM(除非你想寫一個 20 年前的網頁)。
所以我們首先能想到,如果能減少 DOM 操作的次數,那不就變相的提高了 DOM 操作的效能嘛?
方法一:虛擬 DOM 可以將多次 DOM 操作合併為一次操作。
比如你有 1000 個 DOM 節點,通過瀏覽器提供的 DOM API 可能需要操作 1000 次,但是虛擬 DOM 可以合起來只操作 1 次。
方法二:DOM Diff 演算法可以摒除多餘的 DOM 操作
DOM Diff 演算法,顧名思義就是看對比前後兩次 DOM 樹的區別,然後只去更新有變化的 DOM 節點。
經測試,在 8 代 i7+16g 記憶體的 MacBookPro 上,虛擬 DOM 寫入 1000 個 DOM 節點需要 1.58ms;DOM API 寫入耗時 18.55ms
虛擬 DOM 寫入 10,000 個 DOM 節點需要 16.34ms;DOM API 寫入耗時 512.47ms
虛擬 DOM 寫入 50,000 個 DOM 節點需要 128.97ms;DOM API 寫入耗時 1874.72ms
但一旦 DOM 節點數非常高,DOM API 反而會快於虛擬 DOM,因為虛擬 DOM 自身存在大量計算
你的虛擬 DOM,不止是 DOM
虛擬 DOM 並不是 DOM,它只是一個 JS 物件罷了。
那這麼一來他就不僅僅只是 DOM 了,我們還可以用它在安卓、iOS 各個平臺上構建介面!
虛擬 DOM 的“模樣”
來看看 React 的虛擬 DOM 是什麼樣子的:
const vNode = {
key: null,
props: {
children: [
{
// 子元素
key: ...,
props: ...,
children: [...]
...
}
],
className: "",
onClick: () => {}
},
ref: null,
type: "div",
...
}
Vue 的虛擬 DOM 和 React 相比大同小異:
const vNode = {
tag: "div",
data: {
class: "red",
on: {
click: () => {}
}
},
children: [
{.......}
],
...
}
如果你對於 React 和 Vue 比較熟悉的話,相信不用我過多解釋他們的含義。
如何建立一個虛擬 DOM
在 React 中有一個方法,叫做 React.createElement()
。
我們可以這麼使用,來建立一個虛擬 DOM:
createElement(
'div',
{
className: 'red',
onClick: () => {}
},
[
createElement('h1', {}, 'title'),
createElement('span', {}, 'span')
]
)
Vue 的方法和 React 差不多,就不寫了。
到了這你應該發現了一個問題,這麼寫實在是太累了!沒人會這麼寫程式碼的。
React 選擇了 JSX 來代替上面這種“討厭”的寫法。
<div>
<h1></h1>
<span></span>
</div>
通過 babel,這段 JSX 程式碼就會變成 createElement 的形式。(babel 團隊和 React 團隊關係好)
Vue 的 template 語法可以通過 vue-loader 轉化為自己的虛擬 DOM 建立方法。
虛擬 DOM 總結
總而言之,虛擬 DOM 就是一個抽象 DOM 樹的物件,通常包含了「標籤名」、「標籤屬性」、「事件監聽」、「子元素」……
它可以減少不必要的 DOM 操作,還可以跨平臺渲染。
但是虛擬 DOM 也有缺點,那就是他必須通過工廠方法來建立。但這個問題可以通過 JSX、template 等寫法加上編譯外掛來解決。
DOM Diff
虛擬 DOM 會記錄現在的 DOM 樹,和即將更新的 DOM 樹。所以從演算法角度來說,DOM Diff 就是對比兩棵「樹」的不同(包括每個節點的內部屬性)。
逐層對比兩棵樹之後,該演算法會找出需要更新的節點。如果節點是元件就看 Component Diff,如果是標籤就看 Element Diff。
所謂 Component Diff,就是針對元件的 Diff 演算法。
先看元件型別,如果元件型別不同就直接替換;如果型別相同就更新屬性,之後逐層遞迴。
所謂 Element Diff,就是針對標籤的 Diff 演算法
標籤名不一樣直接替換,如果一樣就替換屬性,進而進行遞迴深入。
這裡不得不提一句,為什麼 Vue 和 React 做列表渲染的時候都希望你新增一個 key 屬性呢?
我們簡單看兩個陣列:[1, 2, 3]
和 [1, 3]
。
我們是通過刪除了 2,得到的後面的一個數組。可是 DOM Diff 演算法會認為,這是把 2 修改成了 3,然後把最後一個 3 刪了得到的。
(完)