React虛擬DOM的理解
React虛擬DOM的理解
Virtual DOM
是一棵以JavaScript
物件作為基礎的樹,每一個節點可以將其稱為VNode
,用物件屬性來描述節點,實際上它是一層對真實DOM
的抽象,最終可以通過渲染操作使這棵樹對映到真實環境上,簡單來說Virtual DOM
就是一個Js
物件,是更加輕量級的對DOM
的描述,用以表示整個文件。
描述
在瀏覽器中構建頁面時需要使用DOM
節點描述整個文件。
<div class="root" name="root">
<p>1</p>
<div>11</div>
</div>
如果使用Js
{ type: "tag", tagName: "div", attr: { className: "root" name: "root" }, parent: null, children: [{ type: "tag", tagName: "p", attr: {}, parent: {} /* 父節點的引用 */, children: [{ type: "text", tagName: "text", parent: {} /* 父節點的引用 */, content: "1" }] },{ type: "tag", tagName: "div", attr: {}, parent: {} /* 父節點的引用 */, children: [{ type: "text", tagName: "text", parent: {} /* 父節點的引用 */, content: "11" }] }] }
React中的虛擬DOM
Virtual DOM
是一種程式設計概念,在這個概念裡,UI
以一種理想化的,或者說虛擬的表現形式被保存於記憶體中,並通過如ReactDOM
等類庫使之與真實的DOM
同步,這一過程叫做協調。這種方式賦予了React
宣告式的API
,您告訴React
希望讓UI
是什麼狀態,React
就確保DOM
匹配該狀態,這樣可以從屬性操作、事件處理和手動DOM
更新這些在構建應用程式時必要的操作中解放出來。
與其將Virtual DOM
視為一種技術,不如說它是一種模式,人們提到它時經常是要表達不同的東西。在React
的世界裡,術語Virtual DOM
通常與React
元素關聯在一起,因為它們都是代表了使用者介面的物件,而React
fibers
的內部物件來存放元件樹的附加資訊,上述二者也被認為是React
中Virtual DOM
實現的一部分。
React中的虛擬DOM的歷史
在之前,Facebook
是PHP
大戶,所以React
最開始的靈感就來自於PHP
。
在2004
年這個時候,大家都還在用PHP
的字串拼接來開發網站。
$str = "<ul>";
foreach ($talks as $talk) {
$str += "<li>" . $talk->name . "</li>";
}
$str += "</ul>";
這種方式程式碼寫出來不好看不說,還容易造成XSS
等安全問題。應對方法是對使用者的任何輸入都進行轉義Escape
,但是如果對字串進行多次轉義,那麼反轉義的次數也必須是相同的,否則會無法得到原內容,如果又不小心把HTML
標籤給轉義了,那麼HTML
標籤會直接顯示給使用者,從而導致很差的使用者體驗。
到了2010
年,為了更加高效的編碼,同時也避免轉義HTML
標籤的錯誤,Facebook
開發了XHP
。XHP
是對PHP
的語法拓展,它允許開發者直接在PHP
中使用HTML
標籤,而不再使用字串。
$content = <ul />;
foreach ($talks as $talk) {
$content->appendChild(<li>{$talk->name}</li>);
}
這樣的話,所有HTML
標籤都使用不同於PHP
的語法,我們可以輕易的分辨哪些需要轉義哪些不需要轉義。不久的後來,Facebook
的工程師又發現他們還可以建立自定義標籤,而且通過組合自定義標籤有助於構建大型應用。
到了2013
年,前端工程師Jordan Walke
向他的經理提出了一個大膽的想法:把XHP
的拓展功能遷移到Js
中,首要任務是需要一個拓展來讓JS
支援XML
語法,該拓展稱為JSX
。因為當時由於Node.js
在Facebook
已經有很多實踐,所以很快就實現了JSX
。
const content = (
<TalkList>
{talks.map(talk => <Talk talk={talk} />)}
</TalkList>
);
在這個時候,就有另外一個很棘手的問題,那就是在進行更新的時候,需要去操作DOM
,傳統 DOM API
細節太多,操作複雜,所以就很容易出現Bug
,而且程式碼難以維護。然後就想到了PHP
時代的更新機制,每當有資料改變時,只需要跳到一個由PHP
全新渲染的新頁面即可。
從開發者的角度來看的話,這種方式開發應用是非常簡單的,因為它不需要擔心變更,且介面上使用者資料改變時所有內容都是同步的。為此React
提出了一個新的思想,即始終整體重新整理頁面,當發生前後狀態變化時,React
會自動更新UI
,讓我們從複雜的UI
操作中解放出來,使我們只需關於狀態以及最終UI
長什麼樣。這個時候,我只需要關係我的狀態(資料是什麼),以及UI
長什麼樣(佈局),不再需要關係操作細節。
這種方式雖然簡單粗暴,但是很明顯的缺點,就是很慢。另外還有一個問題就是這樣無法包含節點的狀態,比如它會失去當前聚焦的元素和游標,以及文字選擇和頁面滾動位置,這些都是頁面的當前狀態。
為了解決上面說的問題,對於沒有改變的DOM
節點,讓它保持原樣不動,僅僅建立並替換變更過的DOM
節點,這種方式實現了DOM
節點複用Reuse
。至此,只要能夠識別出哪些節點改變了,那麼就可以實現對DOM
的更新,於是問題就轉化為如何比對兩個DOM
的差異。說到對比差異,可能很容易想到版本控制git
。DOM
是樹形結構,所以diff
演算法必須是針對樹形結構的,目前已知的完整樹形結構的編輯距離diff
演算法複雜度為O(n^3)
。但是時間複雜度O(n^3)
太高了,所以Facebook
工程師考慮到元件的特殊情況,進行了一些優化與折中,然後將複雜度降低到了O(n)
。
DOM
是複雜的,對它的操作尤其是查詢和建立是非常慢非常耗費資源的。看下面的例子,僅建立一個空白的div
,其例項屬性就達到294
個。
// Chrome v84
const div = document.createElement("div");
let m = 0;
for (let k in div) m++;
console.log(m); // 294
對於DOM
這麼多屬性,其實大部分屬性對於做Diff
是沒有任何用處的,所以如果用更輕量級的Js
物件來代替複雜的DOM
節點,然後把對DOM
的diff
操作轉移到Js
物件,就可以避免大量對DOM
的查詢操作。這個更輕量級的Js
物件就稱為Virtual DOM
。那麼現在的過程就是這樣:
- 維護一個使用
Js
物件表示的Virtual DOM
,與真實DOM
一一對應。 - 對前後兩個
Virtual DOM
做diff
,生成變更Mutation
。 - 把變更應用於真實
DOM
,生成最新的真實DOM
。
可以看出,因為要把變更應用到真實DOM
上,所以還是避免不了要直接操作DOM
,但是React
的diff
演算法會把DOM
改動次數降到最低。關於React
中的虛擬DOM
建立過程可以參考https://github.com/facebook/react/blob/9198a5cec0936a21a5ba194a22fcbac03eba5d1d/packages/react/src/ReactElement.js#L348
。
總結
傳統前端的程式設計方式是命令式的,直接操縱DOM
,告訴瀏覽器該怎麼幹,這樣的問題就是,大量的程式碼被用於操作DOM
元素,且程式碼可讀性差,可維護性低。React
的出現,將命令式變成了宣告式,摒棄了直接操作DOM
的細節,只關注資料的變動,DOM
操作由框架來完成,從而大幅度提升了程式碼的可讀性和可維護性。
在初期我們可以看到,資料的變動導致整個頁面的重新整理,這種效率很低,因為可能是區域性的資料變化,但是要重新整理整個頁面,造成了不必要的開銷。所以就有了Diff
過程,將資料變動前後的DOM
結構先進行比較,找出兩者的不同處,然後再對不同之處進行更新渲染。但是由於整個DOM
結構又太大,所以採用了更輕量級的對DOM
的描述—虛擬DOM
。
不過需要注意的是,虛擬DOM
和Diff
演算法的出現是為了解決由指令式程式設計轉變為宣告式程式設計、資料驅動後所帶來的效能問題的。換句話說,直接操作DOM
的效能並不會低於虛擬DOM
和Diff
演算法,甚至還會優於。框架的意義在於為你掩蓋底層的DOM
操作,讓你用更宣告式的方式來描述你的目的,從而讓你的程式碼更容易維護,沒有任何框架可以比純手動的優化DOM
操作更快,因為框架的DOM
操作層需要應對任何上層API
可能產生的操作,它的實現必須是普適的。
虛擬DOM優缺點
優點
Virtual DOM
在犧牲(犧牲很關鍵)部分效能的前提下,增加了可維護性,這也是很多框架的通性。- 實現了對
DOM
的集中化操作,在資料改變時先對虛擬DOM
進行修改,再反映到真實的DOM
,用最小的代價來更新DOM
,提高效率。 - 打開了函式式
UI
程式設計的大門。 - 可以渲染到
DOM
以外的端,使得框架跨平臺,比如ReactNative
,React VR
等。 - 可以更好的實現
SSR
,同構渲染等。 - 元件的高度抽象化。
缺點
- 首次渲染大量
DOM
時,由於多了一層虛擬DOM
的計算,會比innerHTML
插入慢。 - 虛擬
DOM
需要在記憶體中的維護一份DOM
的副本,多佔用了部分記憶體。 - 如果虛擬
DOM
大量更改,這是合適的。但是單一的、頻繁的更新的話,虛擬DOM
將會花費更多的時間處理計算的工作。所以如果你有一個DOM
節點相對較少頁面,用虛擬DOM
,它實際上有可能會更慢,但對於大多數單頁面應用,這應該都會更快。
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://zhuanlan.zhihu.com/p/99973075
https://www.jianshu.com/p/e0a3ac85db5c
https://www.jianshu.com/p/9a1d2750457f
https://github.com/livoras/blog/issues/13
https://juejin.cn/post/6844904165026562056
https://juejin.cn/post/6844903640512086029
https://zh-hans.reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom