[譯] 認識虛擬 DOM

我最近一直在研究 DOM影子 DOM 到底是什麼,以及它們之間有何區別。html

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

另一個術語是「虛擬 DOM 」。雖然這個概念已存在不少年,但在 React 框架中的使用更受歡迎。在這篇文章中,我將詳細闡述什麼是虛擬 DOM 、它跟原始 DOM 的區別以及如何使用。react

爲何咱們須要虛擬 DOM ?

爲了弄明白爲何虛擬 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 。與 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 做爲目標進行修改,這樣的更新是最優的。

原文連接

相關文章
相關標籤/搜索