博客 / 詳情

返回

淺談 Virtual DOM

前言

“Virtual Dom 的優勢是什麼?” 這是一個常見的面試問題,但是答案真的僅僅是簡單粗暴的一句“直接操作dom和頻繁操作dom的性能很差”就完事了嗎?如果是這樣的話,不妨繼續深入地問幾個問題:

  • 直接操作Dom的性能為什麼差?
  • Virtual Dom到底是指什麼?它是如何實現的?
  • 為什麼Virtual Dom能夠避免直接操作dom引起的問題?

    image.png

如果發現自己對這些問題不(yi)太(lian)確(meng)定(bi),那麼不妨往下讀一讀。

正文

Virtual Dom,也就是虛擬的Dom, 無論是在React還是Vue都有用到。它本身並不是任何技術棧所獨有的設計,而是一種設計思路,或者説設計模式。

DOM

在介紹虛擬dom之前,首先來看一下與之相對應的真實Dom:

DOM(Document Object Model)的含義有兩層:

  1. 基於對象來表示的文檔模型(the object-based representation);
  2. 操作這些對象的API;

形如以下的html代碼,

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <h1>Learning Virtual Dom</h1>
    <ul class="list">
        <li class="list-item">List item</li>
    </ul>
</body>
</html>

根據DOM會被表示為如下一棵樹: 樹的每個分支的終點都是一個節點(node),每個節點都包含着對象,包含一些節點屬性。 這就是基於對象來表示文檔
image.png

其次,DOM允許我們通過一些的api對文檔進行操作,例如:

const listItemOne = document.getElementsByClassName("list-item")[0]; // 獲取節點
listItemOne.textContent = "List item one"; // 修改對應的文本內容
const listItemTwo = document.createElement("li"); // 創建一個元素對象
listItemTwo.classList.add("list-item"); // 添加子元素
listItemTwo.textContent = "List item two";
list.appendChild(listItemTwo);

簡而言之。DOM的作用就是把web頁面和腳本(通常是指Javascript)關聯起來

DOM操作帶來的性能問題

那麼原生的DOM操作存在哪些問題呢?在此還需要了解到瀏覽器工作的一些流程,通常來説,一個頁面的生成需要經歷以下步驟:

  1. 解析HTML,產出對應的DOM樹;
  2. 解析CSS, 生成對應的CSS樹;
  3. 將1和2的結果結合生成一棵render樹;
  4. 生成頁面的佈局排列(flow)
  5. 將佈局繪製到顯示設備上(paint)

其中第4步和第5步其實就是常説的頁面渲染,而渲染的過程除了在頁面首次加載時發生,在後續交互過程中,DOM操作也會引起重新排列和重新繪製,渲染是需要較高性能代價的,尤其是重排的過程。

所以常見的優化思路都會提到一點: 為了儘可能減少重繪和重排次數,儘量把改變dom的操作集中在一起,因為寫入操作會觸發重繪或者重排,並且瀏覽器的渲染隊列機制是:當某個操作觸發重排或重繪時,先把該操作放進渲染隊列,等到隊列中的操作到了一定的數量或者到了一定的時間間隔時,瀏覽器就會批量執行。所以集中進行dom操作可以減少重繪重排次數。

另一方面,關於DOM操作的影響範圍問題:由於瀏覽器是基於流式佈局的,所以一旦某個元素重排,它的內部節點會受到影響,而外部節點(兄弟節點和父級節點等等)是有可能不受影響的,這種局部重排引起的影響比較小,所以也需要儘可能地每次只改動最需要的節點元素。

Virtual DOM概覽

Virtual DOM 就是為了解決上面這個問題而生的,它為我們操作dom提供了一種新的方式。

virtual DOM 的本質就是真實dom的一個副本,無需使用DOM API,就可以頻繁地操作和更新此副本。 對虛擬DOM進行所有更新後,我們可以查看需要對原始DOM進行哪些特定更改,並以針對性和優化的方式進行更改.

image.png

這個思路可以參照行軍打仗時的沙盤,沙盤的一個作用就是模擬軍隊的排列分佈。設想一下不借助沙盤時的場景:

將軍1: 我覺得三隊的士兵應該往東邊移動200米,側翼埋伏,然後傳令官跑去通知三隊的士兵,吭哧吭哧跑了200米;

將軍2: 我覺得四隊的士兵應該往西邊移動200米,和三隊形成合圍之勢,然後傳令官繼續通知,四隊的士兵也繼續奔跑。

將軍3:我覺得埋伏的距離太遠了,近一點比較好, 兩隊各向中間移動100米吧。

然後可憐的士兵們繼續來回跑....

image.png

在這個過程裏每次行軍移動都要帶來大量的開銷,每次都直接用實際行動執行還在商討中的指令,成本是很高的。實際上在將軍們探討商量佈陣排列時,可以

  • 先在沙盤上進行模擬排列,
  • 等到得出理想方陣之後,最後再通知到手下的士兵進行對應的調整,

這也就是 Virtual DOM 要做的事。

Virtual DOM 的簡化實現

那麼 Virtual DOM大概是什麼樣呢? 還是按照前面的html文件,對應的virtual dom大概長這樣(不代表實際技術棧的實現,只是體現核心思路):

image.png

const vdom = {
    tagName: "html",// 根節點
    children: [
        { tagName: "head" },
        {
            tagName: "body",
            children: [
                {
                    tagName: "ul",
                    attributes: { "class": "list" },
                    children: [
                        {
                            tagName: "li",
                            attributes: { "class": "list-item" },
                            textContent: "List item"
                        } // end li
                    ]
                } // end ul
            ]
        } // end body
    ]
} // end html

我們用一棵js的嵌套對象樹表示出了dom樹的層級關係以及一些核心屬性,children表示子節點。
在前文我們用原生dom給ul做了一些更新,現在使用Virtual Dom來實現這個過程:

  1. 針對當前的真實DOM複製一份virtual DOM,以及期望改動後的virtual DOM;

    const originalDom = {
    tagName: "html",// 根節點
    children: [
    //省略中間節點
      {
         tagName: "ul",
         attributes: { "class": "list" },
         children: [
             {
                 tagName: "li",
                 attributes: { "class": "list-item" },
                 textContent: "List item"
             }
         ]
      }
    ],
    }
    const newDom = {
    tagName: "html",// 根節點
    children: [
      //省略中間節點
       {
         tagName: "ul",
         attributes: { "class": "list" },
         children: [
             {
                 tagName: "li",
                 attributes: { "class": "list-item" },
                 textContent: "List item one" //改動1,第一個子節點的文本 
             },
             {// 改動2,新增了第二個節點
                 tagName: "li",
                 attributes: { "class": "list-item" },
                 textContent: "List item two"
             }
         ]
      }
     ], 
    };
  2. 比對差異

    const diffRes = [
     {
       newNode:{/*對應上面ul的子節點1*/},
       oldNode:{/*對應上面originalUl的子節點1*/},
     },
     {
       newNode:{/*對應上面ul的子節點2*/},//這是新增節點,所以沒有oldNode
     },
    ]
  3. 收集差異結果之後,發現只要更新list節點,,偽代碼大致如下:

    const domElement = document.getElementsByClassName("list")[0];
    diffRes.forEach((diff) => {
     const newElement = document.createElement(diff.newNode.tagName);
     /* Add attributes ... */
     
     if (diff.oldNode) {
         // 如果存在oldNode則替換
         domElement.replaceChild(diff.newNode, diff.index);
     } else {
         // 不存在則直接新增
         domElement.appendChild(diff.newNode);
     }
    })

    當然,實際框架諸如vuereact裏的diff過程不只是這麼簡單,它們做了更多的優化,例如:

對於有多個項的ul,往其中append一個新節點,可能要引起整個ul所有節點的改動,這個改動成本太高,在diff過程如果遇到了,可能會換一種思路來實現,直接用js生成一個新的ul對象,然後替換原來的ul。這些在後續介紹各個技術棧的文章(可能)會詳細介紹。

可以看到,Virtual DOM的核心思路:先讓預期的變化操作在虛擬dom節點,最後統一應用到真實DOM中去,這個操作一定程度上減少了重繪和重排的機率,因為它做到了:

  1. 將實際dom更改放在diff過程之後, diff的過程有可能經過計算,減少了很多不必要的改變(如同前文將軍3的命令一出,士兵的實際移動其實就變少了);
  2. 對於最後必要的dom操作,也集中在一起處理,貼合瀏覽器渲染機制,減少重排次數;

    小結:回答開頭的問題

現在我們回到開篇的問題--“Virtual Dom 的優勢是什麼?”

在回答這道題之前,我們還需要知道:

  1. 首先,瀏覽器的DOM 引擎、JS 引擎 相互獨立,但是共用主線程;
  2. JS 代碼調用 DOM API 必須 掛起 JS 引擎,激活 DOM 引擎,DOM 重繪重排後,再激活 JS 引擎並繼續執行;
  3. 若有頻繁的 DOM API 調用,瀏覽器廠商不做“批量處理”優化,所以切換開銷和重繪重排的開銷會很大;

而Virtual Dom 最關鍵的地方就是把dom需要做的更改,先放在js引擎裏進行運算,等收集到一定期間的所有dom變更時,這樣做的好處是:

  1. 減少了dom引擎和js引擎的頻繁切換帶來的開銷問題;
  2. 可能在計算比較後,最終只需要改動局部,可以較少很多不必要的重繪重排;
  3. 把必要的Dom操作儘量集中在一起做,減少重排次數

總結

本文從一個常見面試問題出發,介紹了Dom 和Virtual Dom的概念,以及直接操作Dom可能存在的問題,通過對比來説明Virtual Dom的優勢。對於具體技術棧中的Virtual Dom diff過程和優化處理的方式,沒有做較多説明,更專注於闡述Virtual Dom本身的概念。

歡迎大家關注專欄,也希望大家對於喜愛的文章,能夠不吝點贊和收藏,對於行文風格和內容有任何意見的,都歡迎私信交流。

user avatar lanlanjintianhenhappy 頭像 sunhengzhe 頭像 uncletong_doge 頭像 suporka 頭像 201926 頭像 niumingxin 頭像 79px 頭像 qianduanlangzi_5881b7a7d77f0 頭像 pangsir8983 頭像 joytime 頭像 beilee 頭像 huaihuaidedianti 頭像
28 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.