前言
“Virtual Dom 的優勢是什麼?” 這是一個常見的面試問題,但是答案真的僅僅是簡單粗暴的一句“直接操作dom和頻繁操作dom的性能很差”就完事了嗎?如果是這樣的話,不妨繼續深入地問幾個問題:
- 直接操作Dom的性能為什麼差?
- Virtual Dom到底是指什麼?它是如何實現的?
-
為什麼Virtual Dom能夠避免直接操作dom引起的問題?
如果發現自己對這些問題不(yi)太(lian)確(meng)定(bi),那麼不妨往下讀一讀。
正文
Virtual Dom,也就是虛擬的Dom, 無論是在React還是Vue都有用到。它本身並不是任何技術棧所獨有的設計,而是一種設計思路,或者説設計模式。
DOM
在介紹虛擬dom之前,首先來看一下與之相對應的真實Dom:
DOM(Document Object Model)的含義有兩層:
- 基於對象來表示的文檔模型(
the object-based representation); - 操作這些對象的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),每個節點都包含着對象,包含一些節點屬性。 這就是基於對象來表示文檔。
其次,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操作存在哪些問題呢?在此還需要了解到瀏覽器工作的一些流程,通常來説,一個頁面的生成需要經歷以下步驟:
- 解析HTML,產出對應的DOM樹;
- 解析CSS, 生成對應的CSS樹;
- 將1和2的結果結合生成一棵render樹;
- 生成頁面的佈局排列(flow)
- 將佈局繪製到顯示設備上(paint)
其中第4步和第5步其實就是常説的頁面渲染,而渲染的過程除了在頁面首次加載時發生,在後續交互過程中,DOM操作也會引起重新排列和重新繪製,渲染是需要較高性能代價的,尤其是重排的過程。
所以常見的優化思路都會提到一點: 為了儘可能減少重繪和重排次數,儘量把改變dom的操作集中在一起,因為寫入操作會觸發重繪或者重排,並且瀏覽器的渲染隊列機制是:當某個操作觸發重排或重繪時,先把該操作放進渲染隊列,等到隊列中的操作到了一定的數量或者到了一定的時間間隔時,瀏覽器就會批量執行。所以集中進行dom操作可以減少重繪重排次數。
另一方面,關於DOM操作的影響範圍問題:由於瀏覽器是基於流式佈局的,所以一旦某個元素重排,它的內部節點會受到影響,而外部節點(兄弟節點和父級節點等等)是有可能不受影響的,這種局部重排引起的影響比較小,所以也需要儘可能地每次只改動最需要的節點元素。
Virtual DOM概覽
Virtual DOM 就是為了解決上面這個問題而生的,它為我們操作dom提供了一種新的方式。
virtual DOM 的本質就是真實dom的一個副本,無需使用DOM API,就可以頻繁地操作和更新此副本。 對虛擬DOM進行所有更新後,我們可以查看需要對原始DOM進行哪些特定更改,並以針對性和優化的方式進行更改.
這個思路可以參照行軍打仗時的沙盤,沙盤的一個作用就是模擬軍隊的排列分佈。設想一下不借助沙盤時的場景:
將軍1: 我覺得三隊的士兵應該往東邊移動200米,側翼埋伏,然後傳令官跑去通知三隊的士兵,吭哧吭哧跑了200米;
將軍2: 我覺得四隊的士兵應該往西邊移動200米,和三隊形成合圍之勢,然後傳令官繼續通知,四隊的士兵也繼續奔跑。
將軍3:我覺得埋伏的距離太遠了,近一點比較好, 兩隊各向中間移動100米吧。
然後可憐的士兵們繼續來回跑....
在這個過程裏每次行軍移動都要帶來大量的開銷,每次都直接用實際行動執行還在商討中的指令,成本是很高的。實際上在將軍們探討商量佈陣排列時,可以
- 先在沙盤上進行模擬排列,
- 等到得出理想方陣之後,最後再通知到手下的士兵進行對應的調整,
這也就是 Virtual DOM 要做的事。
Virtual DOM 的簡化實現
那麼 Virtual DOM大概是什麼樣呢? 還是按照前面的html文件,對應的virtual dom大概長這樣(不代表實際技術棧的實現,只是體現核心思路):
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來實現這個過程:
-
針對當前的真實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" } ] } ], }; -
比對差異
const diffRes = [ { newNode:{/*對應上面ul的子節點1*/}, oldNode:{/*對應上面originalUl的子節點1*/}, }, { newNode:{/*對應上面ul的子節點2*/},//這是新增節點,所以沒有oldNode }, ] -
收集差異結果之後,發現只要更新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); } })當然,實際框架諸如
vue和react裏的diff過程不只是這麼簡單,它們做了更多的優化,例如:
對於有多個項的ul,往其中append一個新節點,可能要引起整個ul所有節點的改動,這個改動成本太高,在diff過程如果遇到了,可能會換一種思路來實現,直接用js生成一個新的ul對象,然後替換原來的ul。這些在後續介紹各個技術棧的文章(可能)會詳細介紹。
可以看到,Virtual DOM的核心思路:先讓預期的變化操作在虛擬dom節點,最後統一應用到真實DOM中去,這個操作一定程度上減少了重繪和重排的機率,因為它做到了:
- 將實際dom更改放在diff過程之後, diff的過程有可能經過計算,減少了很多不必要的改變(如同前文將軍3的命令一出,士兵的實際移動其實就變少了);
-
對於最後必要的dom操作,也集中在一起處理,貼合瀏覽器渲染機制,減少重排次數;
小結:回答開頭的問題
現在我們回到開篇的問題--“Virtual Dom 的優勢是什麼?”
在回答這道題之前,我們還需要知道:
- 首先,瀏覽器的DOM 引擎、JS 引擎 相互獨立,但是共用主線程;
- JS 代碼調用 DOM API 必須 掛起 JS 引擎,激活 DOM 引擎,DOM 重繪重排後,再激活 JS 引擎並繼續執行;
- 若有頻繁的 DOM API 調用,瀏覽器廠商不做“批量處理”優化,所以切換開銷和重繪重排的開銷會很大;
而Virtual Dom 最關鍵的地方就是把dom需要做的更改,先放在js引擎裏進行運算,等收集到一定期間的所有dom變更時,這樣做的好處是:
- 減少了dom引擎和js引擎的頻繁切換帶來的開銷問題;
- 可能在計算比較後,最終只需要改動局部,可以較少很多不必要的重繪重排;
- 把必要的Dom操作儘量集中在一起做,減少重排次數
總結
本文從一個常見面試問題出發,介紹了Dom 和Virtual Dom的概念,以及直接操作Dom可能存在的問題,通過對比來説明Virtual Dom的優勢。對於具體技術棧中的Virtual Dom diff過程和優化處理的方式,沒有做較多説明,更專注於闡述Virtual Dom本身的概念。
歡迎大家關注專欄,也希望大家對於喜愛的文章,能夠不吝點贊和收藏,對於行文風格和內容有任何意見的,都歡迎私信交流。