1. 程式人生 > 實用技巧 >React虛擬DOM的理解

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的內部物件來存放元件樹的附加資訊,上述二者也被認為是ReactVirtual DOM 實現的一部分。

React中的虛擬DOM的歷史

在之前,FacebookPHP大戶,所以React最開始的靈感就來自於PHP
2004年這個時候,大家都還在用PHP的字串拼接來開發網站。

$str = "<ul>";
foreach ($talks as $talk) {
  $str += "<li>" . $talk->name . "</li>";
}
$str += "</ul>";

這種方式程式碼寫出來不好看不說,還容易造成XSS等安全問題。應對方法是對使用者的任何輸入都進行轉義Escape,但是如果對字串進行多次轉義,那麼反轉義的次數也必須是相同的,否則會無法得到原內容,如果又不小心把HTML標籤給轉義了,那麼HTML標籤會直接顯示給使用者,從而導致很差的使用者體驗。
到了2010年,為了更加高效的編碼,同時也避免轉義HTML標籤的錯誤,Facebook開發了XHPXHP是對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.jsFacebook已經有很多實踐,所以很快就實現了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的差異。說到對比差異,可能很容易想到版本控制gitDOM是樹形結構,所以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節點,然後把對DOMdiff操作轉移到Js物件,就可以避免大量對DOM的查詢操作。這個更輕量級的Js物件就稱為Virtual DOM。那麼現在的過程就是這樣:

  • 維護一個使用Js物件表示的Virtual DOM,與真實DOM一一對應。
  • 對前後兩個Virtual DOMdiff,生成變更Mutation
  • 把變更應用於真實DOM,生成最新的真實DOM

可以看出,因為要把變更應用到真實DOM上,所以還是避免不了要直接操作DOM,但是Reactdiff演算法會把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
不過需要注意的是,虛擬DOMDiff演算法的出現是為了解決由指令式程式設計轉變為宣告式程式設計、資料驅動後所帶來的效能問題的。換句話說,直接操作DOM的效能並不會低於虛擬DOMDiff演算法,甚至還會優於。框架的意義在於為你掩蓋底層的DOM操作,讓你用更宣告式的方式來描述你的目的,從而讓你的程式碼更容易維護,沒有任何框架可以比純手動的優化DOM操作更快,因為框架的DOM操作層需要應對任何上層API可能產生的操作,它的實現必須是普適的。

虛擬DOM優缺點

優點

  • Virtual DOM在犧牲(犧牲很關鍵)部分效能的前提下,增加了可維護性,這也是很多框架的通性。
  • 實現了對DOM的集中化操作,在資料改變時先對虛擬DOM進行修改,再反映到真實的DOM,用最小的代價來更新DOM,提高效率。
  • 打開了函式式UI程式設計的大門。
  • 可以渲染到DOM以外的端,使得框架跨平臺,比如ReactNativeReact 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