我最近一直在研究 DOM 和 影子 DOM 到底是什麼,以及它們之間有何區別。html
歸納地說,文檔對象模型(DOM)包含兩部分;一是 HTML 文檔基於對象的表示,二是操做該對象的一系列接口。影子 DOM 能夠被認爲是 DOM 的縮減版。它也是 HTML 元素基於對象的表示(推薦這篇神奇的Shadow DOM,能更好的理解影子 DOM),影子 DOM 能把 DOM 分離成更小封裝位,而且可以跨 HTML 文檔使用。node
另一個術語是「虛擬 DOM 」。雖然這個概念已存在不少年,但在 React 框架中的使用更受歡迎。在這篇文章中,我將詳細闡述什麼是虛擬 DOM 、它跟原始 DOM 的區別以及如何使用。react
爲了弄明白爲何虛擬 DOM 這個概念會出現,讓咱們從新審視原始 DOM 。正如上面提到的,DOM 有兩部分 —— HTML 文檔的對象表示和一系列操做接口。app
舉個 :chestnut::框架
<!doctype html>
<html lang="en">
<head></head>
<body>
<ul class="list">
<li class="list__item">List item</li>
</ul>
</body>
</html>
複製代碼
上面是一個只包含一條數據的無序列表,可以轉成下面的 DOM 對象: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:ui
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 或 shadow DOM 不一樣,虛擬 DOM 不是官方規範,而是一種與 DOM 交互的新方法。
虛擬 DOM 被認爲是原始 DOM 的副本。此副本可被頻繁地操做和更新,而無需使用 DOM API。一旦對虛擬 DOM 進行了全部更新,咱們就能夠查看須要對原始 DOM 進行哪些特定更改,最後以目標化和最優化的方式進行更改。
「虛擬 DOM 」這個名稱每每會增長這個概念實際上的神祕面紗。實際上,虛擬 DOM 只是一個常規的 Javascript 對象。
回顧以前的 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
複製代碼
與原始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 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 做爲目標進行修改,這樣的更新是最優的。