[譯] 理解虛擬 DOM

正文javascript

我最近寫了關於DOMshadow DOM具體是什麼,以及他們的差別。回顧一下,文檔對象模型是一個HTML文檔基於對象的表示,和操做對象的一個接口。影子DOM(shadow DOM)能夠想作是"精簡版"的DOM。它也是HTML元素基於對象的表示,但不是完整獨立的文檔。相反的,影子DOM容許咱們分離咱們的DOM成爲更小的、具有封裝性的單元並在HTML文檔中使用。html

另外一個類似的術語你可能碰見過的是「虛擬DOM」(virtual DOM)。儘管這個概念已經出現了許多年,讓它變得流行起來的仍是在React框架中的使用。在這篇文章中,我會涉及虛擬DOM具體是什麼,和原始的DOM有什麼差異,以及如何使用。java

爲何咱們須要虛擬DOM?

爲了理解爲何會出現虛擬DOM,先回顧一下原始DOM。就如前面提到的,DOM中有兩部份內容,基於對象的HTML文檔標識和操做對象的API。react

例如,讓咱們看一個無序列表和列表項的簡單HTML文檔bash

<!doctype html>
<html lang="en">
 <head></head>
 <body>
    <ul class="list">
        <li class="list__item">List item</li>
    </ul>
  </body>
</html>
複製代碼

文檔會像下面的DOM樹進行表示app

html
  |
  |-- head lang="en"
  |-- body
        |-- ul class="list"
            |--li class="list__item"
               |--"List item"
複製代碼

設想一下咱們想要修改第一個列表項的內容爲「List item one」,而且添加第二個列表項。爲了作到這一點,咱們須要使用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);
複製代碼

DOM沒法解決的問題

當1998年DOM的初版規範發佈時,咱們構建和管理網頁與如今有很大的不一樣。那時並無像如今這樣依賴於DOM API來建立和更新頁面內容。dom

document.getElementsByClassName()這樣的簡單方法小範圍使用是好的,可是若是咱們每幾秒去更新頁面中的多個元素時,這會讓查詢和更新DOM變得耗性能。工具

更進一步,因爲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或者影子DOM,虛擬DOM並非官方的規範,而是和DOM交互的新方法。

一個虛擬DOM能夠設想爲原始DOM的一份拷貝。這份拷貝能夠不使用DOM的API而進行頻繁的操做和更新。一旦全部的更新都被應用到虛擬DOM後,咱們能夠找到要應用到原始DOM上的具體變化差別並經過高效和有針對性的進行更新。

虛擬DOM看起來像什麼?

虛擬DOM的名字讓這個概念給人有點神祕的感受。實際上,虛擬DOM就是一個常規的JavaScript對象。

讓咱們回顧一下以前建立的DOM樹:

html
  |
  |-- head lang="en"
  |-- body
        |-- ul class="list"
            |--li class="list__item"
               |--"List item"
複製代碼

這顆樹能夠經過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"
                        }
                    ]
                }
            ]
        }
    ]
}
複製代碼

咱們能夠設想這個對象做爲咱們的虛擬DOM。就像原始DOM,這也是基於對象的HTML文檔表示。可是由於這是一個純粹的JavaScript對象,咱們能夠自由和頻繁的操做而不用接觸實際的DOM,直到須要的時候。

一般使用多個小塊的虛擬DOM,而不是使用一個虛擬DOM來表示整個對象。例如,咱們在使用一個list組件,對應咱們的無序列表元素。

const list = {
    tagName: "ul",
    attributes: { "class": "list" },
    children: [
        {
            tagName: "li",
            attributes: { "class": "list__item" },
            textContent: "List item"
        }
    ]
}
複製代碼

虛擬DOM的工做原理

如今咱們看到了虛擬DOM的結構,接下來看一下它是如何在DOM中解決性能和使用問題的。

就如我提到的,咱們可使用虛擬DOM找到須要應用到DOM中的具體變動內容,而後單獨更新這些變動。讓咱們回到無序列表的例子並作一些以前使用DOM API操做的變動。

首先咱們要建立一份虛擬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的變動內容("diff"),在這裏是列表和更新的列表。變動內容以下:

const diffs = [
    {
        newNode: { /* 新版的列表1 */ },
        oldNode: { /* 原始版本的列表1 */ },
        index: /* 在父元素的子節點列表中元素索引 */
    },
    {
        newNode: { /* 列表2 */ },
        index: { /* */ }
    }
]
複製代碼

變動內容提供瞭如何更新實際DOM的指南。一旦全部的變動內容都收集到了,咱們就能夠批量更新DOM,並且是僅更新必需的內容。

例如,咱們能夠遍歷每個的變動內容,而後添加一個新元素或者基於變動內容更新舊的內容。

const domElement = document.getElementsByClassName("list")[0];

diffs.forEach((diff) => {
    const newElement = document.createElement(diff.newNode.tagName);
    /* 添加屬性 */
    
    if (diff.oldNode) {
        // 若是有舊的版本,用新的版本替換
        domElement.replaceChild(newElement, diff.index);
    } else {
        // 若是沒有舊的版本,建立一個新的節點
        domElement.appendChild(newElement);
    }
})
複製代碼

注意這裏只是簡單講述虛擬DOM是如何工做的,有不少案例並無涉及到。

虛擬DOM和框架

一般虛擬DOM會和框架一同使用,而不是像上面這樣直接操做。

像React和Vue這樣的框架使用虛擬DOM來執行高效的DOM更新。例如,咱們的list組件經過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()方法,傳遞一個新的列表。

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是一個可讓咱們和DOM元素進行簡單高效的交互工具。它是DOM的JavaScript對象表示,咱們能夠根據咱們的須要頻繁的修改。對於這個對象的修改會被整理後,有針對性的對實際DOM進行更新。

相關文章
相關標籤/搜索