正文:javascript
我最近寫了關於DOM和shadow DOM具體是什麼,以及他們的差別。回顧一下,文檔對象模型是一個HTML文檔基於對象的表示,和操做對象的一個接口。影子DOM(shadow DOM)能夠想作是"精簡版"的DOM。它也是HTML元素基於對象的表示,但不是完整獨立的文檔。相反的,影子DOM容許咱們分離咱們的DOM成爲更小的、具有封裝性的單元並在HTML文檔中使用。html
另外一個類似的術語你可能碰見過的是「虛擬DOM」(virtual DOM)。儘管這個概念已經出現了許多年,讓它變得流行起來的仍是在React框架中的使用。在這篇文章中,我會涉及虛擬DOM具體是什麼,和原始的DOM有什麼差異,以及如何使用。java
爲了理解爲何會出現虛擬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);
複製代碼
當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的API而進行頻繁的操做和更新。一旦全部的更新都被應用到虛擬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 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會和框架一同使用,而不是像上面這樣直接操做。
像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的JavaScript對象表示,咱們能夠根據咱們的須要頻繁的修改。對於這個對象的修改會被整理後,有針對性的對實際DOM進行更新。