【JS】922- Virtual DOM 認知中的一些誤區

在當下最流行的兩個前端框架都存在 Virtual DOM 的前提下, 漸漸比較多的聽到類似“使用 Virtual DOM 有什麼優勢?” 的面試題,但一直沒有太在意。直到今天在寫一個文檔時,突讓想到要把“爲什麼需要 Virtual DOM ?”也寫進去,待我流暢的寫好答案,略一思索——漏洞百出!也不知道是接納了哪方的知識,讓我一直有能輕鬆回答這個問題的錯覺, 其實對於這個問題我是缺乏思考的。

你或許還不清楚我想說什麼,但請耐下心來,先來看看網絡上關於此問題的一些見解:

虛擬DOM同樣也是操作DOM,爲啥說它快?[1]

  1. 虛擬DOM不會進行排版與重繪操作;
  2. 虛擬DOM進行頻繁修改,然後一次性比較並修改真實DOM中需要改的部分(注意!),最後並在真實DOM中進行排版與重繪,減少過多DOM節點排版與重繪損耗;
  3. 真實DOM頻繁排版與重繪的效率是相當低的;
  4. 虛擬DOM有效降低大面積(真實DOM節點)的重繪與排版,因爲最終與真實DOM比較差異,可以只渲染局部(同2)。

Virtual Dom 的優勢在哪裏?[2]

  1. 具備跨平臺的優勢,由於 Virtual DOM 是以 JavaScript 對象爲基礎而不依賴真實平臺環境,所以使它具有了跨平臺的能力,比如說瀏覽器平臺、Weex、Node 等。
  2. 操作 DOM 慢,js運行效率高。我們可以將DOM對比操作放在JS層,提高效率。因爲DOM操作的執行速度遠不如Javascript的運算速度快,因此,把大量的DOM操作搬運到Javascript中,運用patching算法來計算出真正需要更新的節點,最大限度地減少DOM操作,從而顯著提高性能。
  3. 提升渲染性能 Virtual DOM的優勢不在於單次的操作,而是在大量、頻繁的數據更新下,能夠對視圖進行合理、高效的更新。

Virtual Dom的優勢[3]

  1. 不會立即進行排版與重繪;
  2. VDOM頻繁修改,一次性比較並修改真實DOM中需要修改的部分,最後在真實DOM中進行重排 重繪,減少過多DOM節點重排重繪的性能消耗;
  3. VDOM有效降低大面積真實DOM的重繪與重排,與真實DOM比較差異,進行局部渲染;

上面是從 Google 搜索到的三個平臺中的分析摘選,總結下來大概四點:

  1. 操作 DOM 太慢,操作 Virtual DOM 對象快
  2. 使用 Virtual DOM 可以避免頻繁操作 DOM ,能有效減少迴流和重繪次數(如果有的話)
  3. 有 diff 算法,可以減少沒必要的 DOM 操作
  4. 跨平臺優勢,只要有 JS 引擎就能運行在任何地方(Weex/SSR)

它們的理解正確嗎?本文測試數據都基於 Chrome 86.0.4240.198

Virtual DOM 快?

有人認爲操作 Virtual DOM 速度很快?Virtual DOM 是一個用來描述 DOM(注意,並不一定一一對應)的 Javascript 對象,Javascript 操作 Javascript 對象自然是快的。但 Virtual DOM 仍然需要調用 DOM API 去生成真實的 DOM ,而你其實是可以直接調用它們的,所有就有一個很有意思結論,正數再小也不可能比零還小——Virtual DOM 很快,但這並不是它的優勢,因你本可以選擇不使用 Virtual DOM 。除了速度不是優勢,Virtual DOM 還有個最大的問題——額外的內存佔用,以 Vue 的 Virtual DOM 對象爲例,100W 個空的 Virtual DOM(Vue) 會佔用 110M 內存。內存佔用截圖:

測試代碼:

let creatVNode = function(type) {
  return {
    __v_isVNode: true,
    SKIP: true,
    type,
    props: null,
    key: null,
    ref: null,
    scopeId: 0,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag: 0,
    patchFlag: 0,
    dynamicProps: null,
    dynamicChildren: null,
    appContext: null
  }
}

let counts = 1000000
let list = []
let start = performance.now()
// 創建 VNode(Vue)
// 10000: 1120k
for (let i = 0; i < counts; i++) {
  list.push(creatVNode('div'))
}

// 創建 DOM
// 10000: 320k
// for (let i = 0; i < counts; i++) {
//   list.push(document.createElement('div'))
// }

console.log(performance.now() - start)

_令人意外的是 100W 個空的 DOM 對象只佔用 45M 內存,不清楚在 DOM 屬性明顯更多的情況下 Chrome 是如何優化的,或則是 Dev Tools 存在問題,希望有人能替我解惑。_你看 Virtual DOM 不但執行快沒有用,還增加了大量的內存消耗,所以我們說它快自然是有問題的,因爲沒有 Virtual DOM 時更快。

Virtual DOM 減少迴流和重繪?

也有人認爲 Virtual DOM 能減少頁面的 relayout 和 repaint ?通常有兩個原因來支撐這個觀點:

  1. DOM 操作會先改變 Virtual DOM ,所以一些無效該變(比如把文本 A 修改爲 B ,然後再修改爲 A)就不會調用 DOM API ,也就不會導致瀏覽做無效的迴流和重繪。
  2. DOM 操作會先改變 Virtual DOM ,最終由 Virtual DOM 調用 patch 方法批量操作 DOM ,批量操作就不會導致過程中出現無意義的迴流和重繪。

無效迴流與重繪

第一個觀點看着很有道理,但有個問題很難解釋:瀏覽器的 UI 線程在什麼時候去執行迴流和重繪?要知道現代瀏覽器在設計上爲了避免高複雜度,Javascript 線程和 UI 線程是互斥的,即如果瀏覽器要在 Javascript 執行期間觸發 relayout/repaint 則必須先掛起 Javascript 線程,這是個連我都覺得蠢的設計,顯然不會出現在各大瀏覽器身上。事實上也確實如此,無論你在一次事件循環中調用多少次的 DOM API ,瀏覽器也只會觸發一次迴流與重繪(如果需要),並且如果多次調用並沒有修改 DOM 狀態,那麼迴流與重繪一次都不會發生。Timeline 截圖(沒有迴流和重繪發生):

測試代碼:

<body>
  <div class="app"></div>
  <script> let counts = 1000
    let $app = document.querySelector('.app')
    setTimeout(() => {
      for (let i = 0; i < counts; i++) {
        $app.innerHTML = 'aaaa'
        $app.style = 'margin-top: 100px'
        $app.innerHTML = ''
        $app.style = ''
      }
    }, 1000) </script>
</body>

無意義的迴流與重繪

第二個觀點是比較有意思的,雖然看了上面的分析,你應該也知道它是錯的,批量操作並不能減少迴流與重繪,因爲它們本身就只會觸發一次。但我還是要列出來證明一下,因爲這是我們當下衆多前端的一個固有思維,我在準備寫這篇文章前問了一下衆神交流羣的朋友們,他們幾乎都掉進了這個認知陷阱中,認爲批量操作會減少迴流與重繪。批量操作並不能減少迴流與重繪,原因也和上文一致,Javascript 是單線程且與 UI 線程互斥,所以直接放測試數據:Javascript 執行耗時(數據取3次平均值):

Layout 耗時(數據取3次平均值):

測試代碼:

<body>
  <div class="app"></div>
  <script> let counts = 1000
    let $app = document.querySelector('.app')
    let start = performance.now()
    // 單獨操作
    // for (let i = 0; i < counts; i++) {
    //   let node = document.createTextNode(`${i}, `)
    //   $app.append(node)
    // }

    // 批量操作
    let $tempContainer = document.createElement('div')
    for (let i = 0; i < counts; i++) {
      let node = document.createTextNode('node,')
      $tempContainer.append(node)
    }
    $app.append($tempContainer)

    console.log(performance.now() - start) </script>
</body>

可以看到的是,批量處理和單次處理再 Layout 期間耗時是幾乎一致的,雖然在 script 執行階段還是存在一定的性能優勢(大概 30%),但大抵上只要你用好 DOM 操作,批量或不批量帶來的性能影響是很小的( 10W 次調用多損耗 27ms )。題外話:這裏提出一個問題,爲什麼在 script 執行階段還是存在一定的性能差距?答案會在晚些時候公佈(等我看完這部分邏輯)

Virtual DOM 有 diff 算法?

嚴格來說 diff 算法和 Virtual DOM 是兩個獨立的東西,二者互相之間也沒有充分必要的關聯,比如 svelte[4] 沒有 Virtual DOM 也有其自己的 diff 算法。但由於前端框架存在 Virtual DOM 就總有 diff 算法,並且使用了 Virtual DOM 對 diff 算法也有兩個助力:

  1. 得益於 Virtual DOM 的抽象能力,diff 算法更容易被實現和理解
  2. 得益於 Virtual DOM Tree 總是在內存中, diff 算法功能可以更強大(比如組件移動,沒有完整的 Tree 結構是不可能實現的)

diff 算法能減少 DOM API 調用,顯然是存在設計和性能優勢的,而由於 Virtual DOM 的存在,diff 算法可以更方便且更強大,所以我認同這是 Virtual DOM 的優勢,但不能用“Virtual DOM 有 diff 算法”這樣的表述。

Virtual DOM 有跨平臺優勢?

上文提到的 svelte 沒有 Virtual DOM ,但一樣可以實現服務端渲染,這說明跨平臺並不依賴於 Virtual DOM 。其實只要 Javascript 框架有實現平臺 API 分發機制,就能在不同平臺執行不同的渲染方法,即擁有跨平臺能力。這個能力的根本,是 Javascript 代碼能低代價地在各個平臺運行(得利於瀏覽器在各個平臺的普及和 NodeJS),也就是常說的 Javascript 的優勢之一是跨平臺。所以把跨平臺當做 Virtual DOM 的優勢,其實是不正確的,但我們或許應該去思考下他們爲什麼會這麼認爲。我的想法,可能是這兩個原因:

  1. Virtual DOM 的優勢,可以在不接觸真實 DOM 的情況下操作 DOM,並且性能更好在 Virutal DOM 上的改動,最終還是會調用平臺 API 去操作真實的 DOM ,所以沒有 Virtual DOM 只是相當於少了一箇中間抽象層,並不影響跨平臺能力有無。但還是需要明白,就目前的分析來看,這個抽象層對跨平臺能力還是提供了相當大的方便(或者說助力)的。
  2. Virtual DOM 在 Vue 中很重要,Vue 本身就是一個圍繞 Virtual DOM 創建起來的框架,脫離了 Virtual DOM 其設計思想必然會和當下迥乎不同

總結

本文從互聯網上摘選了部分對開發者對 Virtual DOM 優點的認知,也從現實生活中瞭解到一些誤解,總結爲 Virtual DOM 的四個“優勢”,並分別對這四個“優勢”進行了單獨分析或舉證。最終我們識別了幾個關於 Virtual DOM 優勢誤區:

  1. 操作 DOM 太慢,操作 Virtual DOM 對象快 ❌ (Virtual DOM 很快,但這並不是它的優勢,因你本可以選擇不使用 Virtual DOM );
  2. 使用 Virtual DOM 可以避免頻繁操作 DOM ,能有效減少迴流和重繪次數 ❌ (無論你在一次事件循環中調用多少次的 DOM API ,瀏覽器也只會觸發一次迴流與重繪(如果需要),並且如果多次調用並沒有修改 DOM 狀態,那麼迴流與重繪一次都不會發生。批量操作也不能減少迴流與重繪);
  3. Virtual DOM 有跨平臺優勢 ❌ (跨平臺是 Javascript 的優勢,與 Virtual DOM 無關)。

我們也提到了 Virtual DOM 真正的優點是其抽象能力和常駐內存的特性,讓框架能更容易實現更強大的 diff 算法,缺點是增加了框架複雜度,也佔用了更多的內存。

參考資料

[1]虛擬DOM同樣也是操作DOM,爲啥說它快? (https://segmentfault.com/q/1010000010303981)[2]Virtual Dom 的優勢在哪裏?( https://github.com/RomanHc/blog/issues/20)
[3]Virtual Dom的優勢 (https://juejin.cn/post/6844904179715014669)
[4]svelte(https://github.com/sveltejs/svelte)


  • 本文作者:莫得鹽 

  • 本文鏈接:https://juejin.cn/post/6898526276529684493

1. JavaScript 重溫系列(22篇全)
2. ECMAScript 重溫系列(10篇全)
3. JavaScript設計模式 重溫系列(9篇全)
4.  正則 / 框架 / 算法等 重溫系列(16篇全)
5.  Webpack4 入門(上) ||  Webpack4 入門(下)
6.  MobX 入門(上)  ||   MobX 入門(下)
7. 120 +篇原創系列彙總

回覆“加羣”與大佬們一起交流學習~

點擊“閱讀原文”查看 120+ 篇原創文章

本文分享自微信公衆號 - 前端自習課(FE-study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章