淺談 Virtual DOM

前言

「Virtual Dom 的優點是什麼?」 這是一個常見的面試問題,可是答案真的僅僅是簡單粗暴的一句「直接操做dom和頻繁操做dom的性能不好」就完事了嗎?若是是這樣的話,不妨繼續深刻地問幾個問題:javascript

  • 直接操做Dom的性能爲何差?
  • Virtual Dom究竟是指什麼?它是如何實現的?
  • 爲何Virtual Dom可以避免直接操做dom引發的問題?html

    image.png

若是發現本身對這些問題不(yi)太(lian)確(meng)定(bi),那麼不妨往下讀一讀。vue

正文

Virtual Dom,也就是虛擬的Dom, 不管是在React仍是Vue都有用到。它自己並非任何技術棧所獨有的設計,而是一種設計思路,或者說設計模式。java

DOM

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

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

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

形如如下的html代碼,web

<!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)關聯起來api

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自己的概念。

歡迎你們關注專欄,也但願你們對於喜好的文章,可以不吝點贊和收藏,對於行文風格和內容有任何意見的,都歡迎私信交流。

相關文章
相關標籤/搜索