TAT.李強 React 虛擬 DOM 淺析
In 未分類 on 2015年10月31日 by view: 29,313
16

       在 Web 開發中,需要將數據的變化實時反映到 UI 上,這時就需要對 DOM 進行操作,但是復雜或頻繁的 DOM 操作通常是性能瓶頸產生的原因,為此,React 引入了虛擬 DOM(Virtual DOM)的機制。

  1. 什么是虛擬 DOM?
  2. 虛擬 DOM VS 直接操作原生 DOM?
  3. 虛擬 DOM VS MVVM?
  4. 對 React 虛擬 DOM 的誤解?


一、什么是虛擬 DOM?

        在 React 中,render 執行的結果得到的并不是真正的 DOM 節點,結果僅僅是輕量級的 JavaScript 對象,我們稱之為 virtual DOM。

        虛擬 DOM 是 React 的一大亮點,具有 batching(批處理) 和高效的 Diff 算法。這讓我們可以無需擔心性能問題而” 毫無顧忌” 的隨時“ 刷新” 整個頁面,由虛擬 DOM 來確保只對界面上真正變化的部分進行實際的 DOM 操作。在實際開發中基本無需關心虛擬 DOM 是如何運作的,但是理解其運行機制不僅有助于更好的理解 React 組件的生命周期,而且對于進一步優化 React 程序也會有很大幫助。

二、虛擬 DOM VS 直接操作原生 DOM?

       如果沒有 Virtual DOM,簡單來說就是直接重置 innerHTML。這樣操作,在一個大型列表所有數據都變了的情況下,還算是合理,但是,當只有一行數據發生變化時,它也需要重置整個 innerHTML,這時候顯然就造成了大量浪費。

比較 innerHTML 和 Virtual DOM 的重繪過程如下:

innerHTML: render html string + 重新創建所有 DOM 元素

Virtual DOM: render Virtual DOM + diff + 必要的 DOM 更新

        和 DOM 操作比起來,js 計算是非常便宜的。Virtual DOM render + diff 顯然比渲染 html 字符串要慢,但是,它依然是純 js 層面的計算,比起后面的 DOM 操作來說,依然便宜了太多。當然,曾有人做過驗證說 React 的性能不如直接操作真實 DOM,代碼如下:

        該測試用例中雖然構造了一個包含 1000 個 Tag 的 String,并把它添加到 DOM 樹中,但是只做了一次 DOM 操作。然而,在實際開發過程中,這 1000 個元素更新可能分布在 20 個邏輯塊中,每個邏輯塊中包含 50 個元素,當頁面需要更新時,都會引起 DOM 樹的更新,上述代碼就近似變成了如下格式:

         這樣來看,React 的性能就遠勝于原生 DOM 操作了。

        而且,DOM 完全不屬于 Javascript (也不在 Javascript 引擎中存在).。Javascript 其實是一個非常獨立的引擎,DOM 其實是瀏覽器引出的一組讓 Javascript 操作 HTML 文檔的 API 而已。在即時編譯的時代,調用 DOM 的開銷是很大的。而 Virtual DOM 的執行完全都在 Javascript 引擎中,完全不會有這個開銷。

        React.js 相對于直接操作原生 DOM 有很大的性能優勢, 很大程度上都要歸功于 virtual DOM 的 batching 和 diff。batching 把所有的 DOM 操作搜集起來,一次性提交給真實的 DOM。diff 算法時間復雜度也從標準的的 Diff 算法的 O(n^3) 降到了 O(n)。這里留到下一次博客單獨講。

三、虛擬 DOM VS MVVM?

         相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是數據綁定:通過 Directive/Binding 對象,觀察數據變化并保留對實際 DOM 元素的引用,當有數據變化時進行對應的操作。MVVM 的變化檢查是數據層面的,而 React 的檢查是 DOM 結構層面的。MVVM 的性能也根據變動檢測的實現原理有所不同:Angular 的臟檢查使得任何變動都有固定的 O(watcher count) 的代價;Knockout/Vue/Avalon 都采用了依賴收集,在 js 和 DOM 層面都是 O(change):

  • 臟檢查:scope digest + 必要 DOM 更新
  • 依賴收集:重新收集依賴 + 必要 DOM 更新

        可以看到,Angular 最不效率的地方在于任何小變動都有的和 watcher 數量相關的性能代價。但是!當所有數據都變了的時候,Angular 其實并不吃虧。依賴收集在初始化和數據變化的時候都需要重新收集依賴,這個代價在小量更新的時候幾乎可以忽略,但在數據量龐大的時候也會產生一定的消耗。
        MVVM 渲染列表的時候,由于每一行都有自己的數據作用域,所以通常都是每一行有一個對應的 ViewModel 實例,或者是一個稍微輕量一些的利用原型繼承的 "scope" 對象,但也有一定的代價。所以,MVVM 列表渲染的初始化幾乎一定比 React 慢,因為創建 ViewModel / scope 實例比起 Virtual DOM 來說要昂貴很多。這里所有 MVVM 實現的一個共同問題就是在列表渲染的數據源變動時,尤其是當數據是全新的對象時,如何有效地復用已經創建的 ViewModel 實例和 DOM 元素。假如沒有任何復用方面的優化,由于數據是 “ 全新” 的,MVVM 實際上需要銷毀之前的所有實例,重新創建所有實例,最后再進行一次渲染!這就是為什么題目里鏈接的 angular/knockout 實現都相對比較慢。相比之下,React 的變動檢查由于是 DOM 結構層面的,即使是全新的數據,只要最后渲染結果沒變,那么就不需要做無用功。
        Angular 和 Vue 都提供了列表重繪的優化機制,也就是 “ 提示” 框架如何有效地復用實例和 DOM 元素。比如數據庫里的同一個對象,在兩次前端 API 調用里面會成為不同的對象,但是它們依然有一樣的 uid。這時候你就可以提示 track by uid 來讓 Angular 知道,這兩個對象其實是同一份數據。那么原來這份數據對應的實例和 DOM 元素都可以復用,只需要更新變動了的部分?;蛘?,你也可以直接 track by $index 來進行 “ 原地復用”:直接根據在數組里的位置進行復用。在題目給出的例子里,如果 angular 實現加上 track by $index 的話,后續重繪是不會比 React 慢多少的。甚至在 dbmonster 測試中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默認版本無優化,優化過的在下面)

        在比較性能的時候,要分清楚初始渲染、小量數據更新、大量數據更新這些不同的場合。Virtual DOM、臟檢查 MVVM、數據收集 MVVM 在不同場合各有不同的表現和不同的優化需求。Virtual DOM 為了提升小量數據更新時的性能,也需要針對性的優化,比如 shouldComponentUpdate 或是 immutable data。

  • 初始渲染:Virtual DOM > 臟檢查 >= 依賴收集
  • 小量數據更新:依賴收集 >> Virtual DOM + 優化 > 臟檢查(無法優化)> Virtual DOM 無優化
  • 大量數據更新:臟檢查 + 優化 >= 依賴收集 + 優化 > Virtual DOM(無法/無需優化)>> MVVM 無優化

(該段落借鑒了知乎的相關回答)

四、對 React 虛擬 DOM 的誤解?

        React 從來沒有說過 “React 比原生操作 DOM 快”。React 給我們的保證是,在不需要手動優化的情況下,它依然可以給我們提供過得去的性能。

        React 掩蓋了底層的 DOM 操作,可以用更聲明式的方式來描述我們目的,從而讓代碼更容易維護。下面還是借鑒了知乎上的回答:沒有任何框架可以比純手動的優化 DOM 操作更快,因為框架的 DOM 操作層需要應對任何上層 API 可能產生的操作,它的實現必須是普適的。針對任何一個 benchmark,我都可以寫出比任何框架更快的手動優化,但是那有什么意義呢?在構建一個實際應用的時候,你難道為每一個地方都去做手動優化嗎?出于可維護性的考慮,這顯然不可能。

原創文章轉載請注明:

轉載自AlloyTeam:http://www.ecomenagepro.com/2015/10/react-virtual-analysis-of-the-dom/

  1. 顏海鏡 2017 年 10 月 31 日

    學習

  2. monkindey 2016 年 6 月 24 日

    這篇不是 https://www.zhihu.com/question/31809713 知乎上說的么?

  3. aaron 2015 年 11 月 30 日

    大概是怎么 diff 的,以及 diff 后怎么更新到 dom 上的 。這最重要的地方沒講到

    • TAT.李強

      TAT.李強 2015 年 12 月 14 日

      是的,這里東西比較多,下次單獨講,文中提了的,還望繼續關注。

  4. yimity 2015 年 11 月 10 日

    這里還是沒有說清楚具體 Virtual DOM diff 之后是如何將這些 dom 更新到 ui 中的。
    例如有個例子:有 1000 個 li ,每個 li 里面有三項 a p
    span(具體是什么不重要)內容,那我如果更新某一個 li 里面的某一項 a 的內容的話,可能 react 快于 angular。但是假如我 1000 個 li 里面都有一項 p 被更新了,這時候,具體 react 是如何處理的?如果是單獨更新每一個變化項 那么 react 要最終會操作 1000 次 dom 而 angular 可能只有一次。
    求解釋。

    • TAT.李強

      TAT.李強 2015 年 12 月 14 日

      是的,關于 diff 和 batching 下次會單獨講一下,希望繼續關注哈。先結合你這個例子說一下哈,在列表項發生變化的時候,react 會先和上一次緩存的 virtual dom 做 diff,這個過程會計算出從現有的真實 dom 到新的真實 dom 需要的最少操作,然后通過 dom api 作用于現在的真實 dom 上,渲染得到新的 dom。對該例而言,和代碼實現也有關系,diff 有三個先驗條件,對于組件樹只比較同層級的、基于組件、基于屬性 key。如果對每個列表項都設置了唯一的 key,那么確實是要有 1000 次 dom 操作,就像你說的,肯定性能不好,而如果代碼實現上是基于組件的,那么 react 就會直接移除整個組件,不會對組件內部元素做 diff,這樣也是執行了一次刪除一次重建,但是這樣實現 react 并不支持。所以對于一次更新一個大列表項來說,react 的性能并不算好。對于 angular 來說,也和代碼實現有關,他也并不是只有一次 dom 操作那么簡單,angular 建議一個頁面最多 2000 個雙向綁定。不知道有沒有說清楚,下次會分享 diff 的相關問題,希望看過后再給意見。其實個人感覺 react 最大的優點并不是它的性能怎么樣,而是它組件化、模塊化的思想,對前端代碼的規范性做出了巨大貢獻,降低了多人維護成本。

  5. react新手 2015 年 11 月 3 日

    web 工程師們不是都說 avalon 遠超 angular 嗎?應該用 avalon 與 react 對比看看

    • 亦馳fantasy 2015 年 11 月 12 日

      我怎么沒聽說過…

      • react黑 2015 年 11 月 25 日

        你去百度看看

        • avalon新手 2015 年 11 月 25 日

          avalon 作者是這樣稱贊 react 的:在 View 減少冗余的 DOM 操作,必須減少一次事務中對同一個元素的重復操作,Angular 有 $apply 手動觸發,avalon 可以用 $unwatch、$watch,但都不太好用。這難題最后被 Facebook 的新銳視圖庫 React 搞定了,它號稱是使用了一種叫 Virtual DOM 的技術搞定。顯然,這答案沒有暴露其全貌,其他使用 Virtual DOM 的庫,性能也很難追得上 React。

          React 為了提高性能,其最核心的架子是其基于層次結構的 UUID 技術。有了它,才能實現節點的最小化更新。比如,一個對象里面有 30 個鍵值對,后來更新該對象,換成另外 11 個新的鍵值對,這樣我們只要去掉頁面多出的 19 個 DOM 節點,再修改已有 11 個節點內容或屬性即可。如果你的框架使用靜態模板來實現這一功能,這 30 個節點需要重新創建與插入,性能肯定也沒有這么好。

    • TAT.李強

      TAT.李強 2015 年 12 月 14 日

      并不能說哪個框架優于哪個框架,上文也是以框架的實現原理進行對比的,并不是說拿 angular 或者 avalon 與 react 對比,而是以底層實現機制做的對比,涵蓋了 angular 和 avalon。在不同場景下,每個框架的表現也不盡相同,所以不能一棒子說死,說哪一個更好。

  6. 我這是好的 2015 年 11 月 3 日

    為啥光比渲染速度,不比內存占用呢?

    • TAT.李強

      TAT.李強 2015 年 12 月 14 日

      這個問題提的好,下次會在 diff 的時候一起說一下。其實我們都明白的是,瀏覽器渲染 dom 是最耗內存的,react 引入 virtual dom 來減少內存的開支,在 js engine 層面先進行 diff,計算出從現有的 dom 到更改后的 dom 需要的最小操作(這里都是 js 層面的),在通過 dom api 作用于現有的 dom 上,渲染得到新的 dom??梢哉f react 的推出,也是 facebook 工程師們出于節省內存的考慮。

      • yan.O 2016 年 6 月 27 日

        這個也未必,比 mvvm 少

  7. 澤愷 2015 年 11 月 2 日

    正如上文中的一句話:“如果 angular 實現加上 track by $index 的話,后續重繪是不會比 React 慢多少的”。所以盲目的說 React 比 Angular 快的人是不理智的啊

發表評論