[譯] 認識虛擬 DOM

Jouryjc發表於2019-01-05

我最近一直在研究 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

假設我們想要將第一個列表項的內容修改為“列出專案一”,並新增第二個列表項。為此,我們需要使用 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 樹:

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

原文連結

相關文章