我最近一直在研究 DOM 和 影子 DOM 究竟是什麼,以及它們之間有何區別。
概括地說,文件物件模型(DOM)包含兩部分;一是 HTML 文件基於物件的表示,二是操作該物件的一系列介面。影子 DOM 可以被認為是 DOM 的縮減版。它也是 HTML 元素基於物件的表示(推薦這篇神奇的Shadow DOM,能更好的理解影子 DOM),影子 DOM 能把 DOM 分離成更小封裝位,並且能夠跨 HTML 文件使用。
另外一個術語是“虛擬 DOM ”。雖然這個概念已存在很多年,但在 React 框架中的使用更受歡迎。在這篇文章中,我將詳細闡述什麼是虛擬 DOM 、它跟原始 DOM 的區別以及如何使用。
為什麼我們需要虛擬 DOM ?
為了弄明白為什麼虛擬 DOM 這個概念會出現,讓我們重新審視原始 DOM 。正如上面提到的,DOM 有兩部分 —— HTML 文件的物件表示和一系列操作介面。
舉個 :chestnut::
<!doctype html>
<html lang="en">
<head></head>
<body>
<ul class="list">
<li class="list__item">List item</li>
</ul>
</body>
</html>
複製程式碼
上面是一個只包含一條資料的無序列表,能夠轉成下面的 DOM 物件:
假設我們想要將第一個列表項的內容修改為“列出專案一”,並新增第二個列表項。為此,我們需要使用 DOM API 來查詢我們想要更新的元素,建立新元素,新增屬性和內容,然後最終更新 DOM 元素本身。
const listItemOne = document.getElementsByClassName("list__item")[0];
listItemOne.textContent = "List item one";
const list = document.getElementsByClassName("list")[0];
const listItemTwo = document.createElement("li");
listItemTwo.classList.add("list__item");
listItemTwo.textContent = "List item two";
list.appendChild(listItemTwo);
複製程式碼
我們現在建立網頁的方式跟1998年發行的第一版 DOM 不同,他們不像我們今天這麼頻繁的依賴 DOM API。
舉例一些簡單的方法,比如 document.getElementsByClassName()
可以小規模使用,但如果每秒更新很多元素,這非常消耗效能。
更進一步,由於 API 的設定方式,一次性更新大篇文件會比查詢和更新特定的文件更節省效能。回到前面列表的 :chestnut:
const list = document.getElementsByClassName("list")[0];
list.innerHTML = `<li class="list__item">List item one</li>
<li class="list__item">List item two</li>`;
複製程式碼
替換整個無序列表會比修改特定元素更好。在這個特定的 :chestnut: ,上述兩種方法效能差異可能是微不足道的。但是,隨著網頁規模不斷增大,這種差異會越來越明顯。
什麼是虛擬 DOM ?
建立虛擬 DOM 是為了更高效、頻繁地更新 DOM 。與 DOM 或 shadow DOM 不同,虛擬 DOM 不是官方規範,而是一種與 DOM 互動的新方法。
虛擬 DOM 被認為是原始 DOM 的副本。此副本可被頻繁地操作和更新,而無需使用 DOM API。一旦對虛擬 DOM 進行了所有更新,我們就可以檢視需要對原始 DOM 進行哪些特定更改,最後以目標化和最優化的方式進行更改。
“虛擬 DOM ”這個名稱往往會增加這個概念實際上的神祕面紗。實際上,虛擬 DOM 只是一個常規的 Javascript 物件。
回顧之前的 DOM 樹:
上述這顆樹可以用下面的 Javascript 物件表示: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
複製程式碼
與原始DOM一樣,它是我們的 HTML 文件基於物件的表示。因為它是一個簡單的 Javascript 物件,我們可以隨意並頻繁地操作它,而無須觸及真實的 DOM 。
不一定要使用整個物件,更常見是使用小部分的虛擬 DOM 。例如,我們可以處理列表元件,它將對無序列表元素進行相應的處理。
const list = {
tagName: "ul",
attributes: { "class": "list" },
children: [
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item"
}
]
};
複製程式碼
虛擬 DOM 的原理
現在我們已經知道了虛擬 DOM 是什麼,但它是如何解決操作 DOM 的效能問題呢?
正如我所提到的,我們可以使用虛擬 DOM 來挑選出需要對 DOM 進行的特定更改,並單獨進行這些特定更新。回到無序列表示的例子,並使用虛擬 DOM 進行相同的更改。
我們要做的第一件事是製作虛擬 DOM 的副本,其中包含我們想要的修改。我們無須使用 DOM API,因此我們只需建立一個新物件。
const copy = {
tagName: "ul",
attributes: { "class": "list" },
children: [
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item one"
},
{
tagName: "li",
attributes: { "class": "list__item" },
textContent: "List item two"
}
]
};
複製程式碼
此副本用於在原始虛擬 DOM(在本例中為列表)和更新的虛擬 DOM 之間建立所謂的“差異”。差異可能看起來像這樣:
const diffs = [
{
newNode: { /* new version of list item one */ },
oldNode: { /* original version of list item one */ },
index: /* index of element in parent's list of child nodes */
},
{
newNode: { /* list item two */ },
index: { /* */ }
}
]
複製程式碼
上述物件提供了節點資料更新前後的差異。一旦收集了所有差異,我們就可以批量更改 DOM,並只做所需的更新。
例如,我們可以迴圈遍歷每個差異,並根據 diff 指定的內容新增新的子代或更新舊的子代。
const domElement = document.getElementsByClassName("list")[0];
diffs.forEach((diff) => {
const newElement = document.createElement(diff.newNode.tagName);
/* Add attributes ... */
if (diff.oldNode) {
// If there is an old version, replace it with the new version
domElement.replaceChild(diff.newNode, diff.index);
} else {
// If no old version exists, create a new node
domElement.appendChild(diff.newNode);
}
})
複製程式碼
框架
通過框架使用虛擬 DOM 更常見。諸如 React 和 Vue 之類的框架使用虛擬 DOM 概念來對 DOM 進行更高效的更新。例如,我們的列表元件可以用以下方式用 React 編寫。
import React from 'react';
import ReactDOM from 'react-dom';
const list = React.createElement("ul", { className: "list" },
React.createElement("li", { className: "list__item" }, "List item")
);
ReactDOM.render(list, document.body);
複製程式碼
如果我們要更新列表,重寫整個列表模板,並呼叫 ReactDOM.render()
:
const newList = React.createElement("ul", { className: "list" },
React.createElement("li", { className: "list__item" }, "List item one"),
React.createElement("li", { className: "list__item" }, "List item two");
);
setTimeout(() => ReactDOM.render(newList, document.body), 5000);
複製程式碼
因為 React 使用虛擬 DOM ,即使我們重新渲染整個模板,也只更新實際存在差異的部分。
總結
回顧一下,虛擬 DOM 是一種工具,使我們能夠以更簡單,更高效的方式與 DOM 元素進行互動。它是 DOM 的 Javascript 物件表示,我們可以根據需求隨時修改。然後整理對該物件所做的所有修改,並以實際 DOM 作為目標進行修改,這樣的更新是最優的。