天天看點

[譯] 認識虛拟 DOM

我最近一直在研究 DOM 和 影子 DOM 究竟是什麼,以及它們之間有何差別。

概括地說,文檔對象模型(DOM)包含兩部分;一是 HTML 文檔基于對象的表示,二是操作該對象的一系列接口。影子 DOM 可以被認為是 DOM 的縮減版。它也是 HTML 元素基于對象的表示(推薦這篇神奇的Shadow DOM,能更好的了解影子 DOM),影子 DOM 能把 DOM 分離成更小封裝位,并且能夠跨 HTML 文檔使用。

另外一個術語是“虛拟 DOM ”。雖然這個概念已存在很多年,但在 React 架構中的使用更受歡迎。在這篇文章中,我将詳細闡述什麼是虛拟 DOM 、它跟原始 DOM 的差別以及如何使用。

為什麼需要虛拟 DOM?

為了弄明白為什麼虛拟 DOM 這個概念會出現,讓我們重新審視原始 DOM 。正如上面提到的,DOM 有兩部分 —— HTML 文檔的對象表示和一系列操作接口。

舉個 🌰:

<!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 的設定方式,一次性更新大篇文檔會比查找和更新特定的文檔更節省性能。回到前面清單的 🌰

const list = document.getElementsByClassName("list")[0];
list.innerHTML = `<li class="list__item">List item one</li>
  <li class="list__item">List item two</li>`;
           

複制

替換整個無序清單會比修改特定元素更好。在這個特定的 🌰 ,上述兩種方法性能差異可能是微不足道的。但是,随着網頁規模不斷增大,這種差異會越來越明顯。

什麼是虛拟 DOM?

建立虛拟 DOM 是為了更高效、頻繁地更新 DOM 。與 DOM 或 shadow DOM 不同,虛拟 DOM 不是官方規範,而是一種與 DOM 互動的新方法。

虛拟 DOM 被認為是原始 DOM 的副本。此副本可被頻繁地操作和更新,而無需使用 DOM API。一旦對虛拟 DOM 進行了所有更新,我們就可以檢視需要對原始 DOM 進行哪些特定更改,最後以目标化和最優化的方式進行更改。

“虛拟 DOM ”這個名稱往往會增加這個概念實際上的神秘面紗。實際上,虛拟 DOM 隻是一個正常的 Javascript 對象。

回顧之前的 DOM 樹:

[譯] 認識虛拟 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 作為目标進行修改,這樣的更新是最優的。

原文位址:https://bitsofco.de/understanding-the-virtual-dom/