當我們更新資料和渲染畫面時會頻繁地新增和刪除 DOM 元素,造成效能問題。因此,不論是 Vue 或 React 都有使用 Virtual DOM 來避免直接操控 DOM。當 Vue 偵測到資料有更新,就會再次渲染更新的部分。但為了效能的考慮,它不會把整個 DOM 換掉,而是先建立一個 Virtual DOM,與原本的 DOM 作出比較,並透過算法找出有差異的部分,再針對它們來更新舊有的 DOM。
以下會再作詳細解說。
在進入主題前,先稍為重溫什麼是 DOM。DOM 是以樹狀結構來顯示一個 HTML 文件的模型,這個樹模型由一個個 DOM 節點(元素)組成。Web APIs 有提供 document.createElement
、 document.body.appendChild()
等語法來操控 DOM。
圖片來源:https://www.runoob.com/js/js-htmldom.html
以上圖為例,左邊有一個 h1
標籤,所以在該 HTML 文件中,應該會找到這個 DOM 節點:
<h1> 標題 </h1>
Virtual DOM 是透過 JavaScript 物件模擬 一個 DOM 節點(Node)。之後再經由負責渲染的函式,變成真正的 DOM 節點,最後被掛載到網頁裏,完成更新 DOM。
Vue 的 Virtual DOM 是參考 Snabbdom 這個 Virtual DOM 套件來實現。在 Vue 我們把 Virtual DOM 稱為 VNode。
簡單總括流程如下:
關於第二點,之前的文章有提到 Vue 使用了 MVVM 模式:
View(畫面) <---> ViewModel <---> Model (資料)
ViewModel 會監聽 View 各種 DOM 的事件,並與相關的 Model 作綁定。當畫面觸發事件,就會觸發修改資料,之後再跑渲染資料到畫面的流程。
Vue 使用 diff 算法來避免在更新 DOM 時把整個 DOM 全都更新,而是只針對有更動的 DOM 來作更新,提升效能。
在此簡單總括 diff 算法。當資料有更動時,Vue 就會產生兩個 DOM。一是 Virtual DOM,二是原本舊有的 DOM。此算法就會以同層級比較的方式,比較兩者的 DOM 節點的差異,並針對作出更新。有關詳細針對 Vue 原始碼和概念解說,可參考這篇文章。
算法的部分就不在此深究,但可以試試用 JavaScript 練習實作一個 Virtual DOM 來理解概念。例如我想建立一個 DOM:
<div id="app">
<a href="https://google.com">
這是一段文字
</a>
</div>
用 JavaScript 物件來表示:
const exampleNode = {
tagName: "div",
attrs: {
id: "app",
},
children: [
{
tagName: "a",
attrs: {
href: "https://google.com",
},
children: ["這是一段文字"],
},
],
};
按這樣的結構,我們先建立一個負責產生 Virtual DOM 的 Class
:
class Element {
constructor(tagName, { attrs = {}, children = [] }) {
this.tagName = tagName;
this.attrs = attrs;
this.children = children;
}
}
我們會用 Element
這個 Class 來建構出 Virtual DOM 的物件。接下來把之前提到的 DOM 結構,透過使用 Element
Class 來建構:
const VNode = new Element("div", {
attrs: {
id: "app",
},
children: [
new Element("a", {
attrs: {
href: "https://google.com",
},
children: ["這是一段文字"],
}),
],
});
用 console.log(VNode)
查看目前 VNode
的值:
目前完成以 JavaScript 物件方式來顯示我們想要達成的 DOM 結構。接下來就是把它轉換為真正的 DOM。在 Element
加入 renderElement
函式,並使用 document.createElement
、Element.setAttribute()
及 Element.appendChild
等 Web APIs 來建立真正的 DOM:
class Element {
constructor(tagName, { attrs = {}, children = [] }) {
this.tagName = tagName;
this.attrs = attrs;
this.children = children;
}
renderElement() {
const element = document.createElement(this.tagName);
for (const [attrName, attrValue] of Object.entries(this.attrs)) {
element.setAttribute(attrName, attrValue);
}
this.children.forEach((child) => {
const childElement =
// 如果此 child 不是以 Element 建構出來,就代表它是 textNode
child instanceof Element
? child.renderElement()
: document.createTextNode(child);
// 把子 Node 塞進 父 Node 裏
element.appendChild(childElement);
});
return element;
}
}
呼叫 renderElement
的方法,就能產出一個真正的 DOM:
const result = VNode.renderElement()
用 console.log(result)
看看結果:
最後把它掛載到網頁上:
document.getElementById("app").appendChild(result);
https://codesandbox.io/s/yong-javascript-wu-jian-shi-zuo-virtual-dom-0wcjj?file=/src/index.js
Vue.js 技术揭秘 - Virtual DOM
深度剖析:如何实现一个 Virtual DOM 算法
從頭打造一個簡單的 Virtual DOM