虛擬 DOM 前世今生
虛擬 DOM 也叫 VDOM,虛擬 DOM 其實并不是什么新鮮事物度液,早在多年前網(wǎng)上就已經(jīng)有很多介紹虛擬 DOM 的文章厕宗。
但把虛擬 DOM 發(fā)揚光大的是 React,并且 vue2.0 也引入虛擬 DOM堕担,可以看出虛擬 DOM 在前端舉足輕重的地位已慢。
簡單來說虛擬 DOM 就是用數(shù)據(jù)格式表示 DOM 結(jié)構(gòu),并沒有真實的 append 到 DOM 上霹购,因此稱為虛擬 DOM佑惠。
虛擬 DOM 有何作用
使用虛擬 DOM 帶來的好處是顯而易見:和瀏覽器交互去操作 DOM 的效率遠不及去操作數(shù)據(jù)結(jié)構(gòu)。操作數(shù)據(jù)結(jié)構(gòu)是指改變“虛擬 DOM 對象”厕鹃,這個過程比修改真實 DOM 快很多兢仰。
不過使用虛擬 DOM 并不能使得操作 DOM 的數(shù)量減少,因為虛擬 DOM 也最終是要掛載到瀏覽器上成為真實 DOM 節(jié)點剂碴,但能夠精確的獲取最小的、最必要的操作 DOM 的集合轻专。
這樣一來忆矛,我們抽象表示 DOM,每次通過虛擬 DOM 的 diff 算法計算出視圖前后更新的最小差異,再根據(jù)這個最小差異去渲染/更新真實的 DOM催训,無疑更為可靠洽议,性能更高。
創(chuàng)建虛擬 DOM
說了這么多漫拭,到底該如何創(chuàng)建虛擬 DOM 呢亚兄?
我們仿造一些主流的虛擬 DOM 庫的思想去實現(xiàn)一個簡易版的虛擬 DOM。
如現(xiàn)有如下 DOM 結(jié)構(gòu):
<ul id="ul1">
<li class="li-stl1">item1</li>
<li class="li-stl1">item2</li>
<li class="li-stl1">item3</li>
</ul>
現(xiàn)在如果要用 js 來表示采驻,我們構(gòu)建這樣一個對象結(jié)構(gòu):
const myVirtualDom = {
tagName: "ul",
attributes: {
id: "ul1",
},
children: [
{ tagName: "li", attributes: { class: "li-stl1" }, children: ["item1"] },
{ tagName: "li", attributes: { class: "li-stl1" }, children: ["item2"] },
{ tagName: "li", attributes: { class: "li-stl1" }, children: ["item3"] },
],
};
- tagName 表示真實 DOM 標簽類型审胚;
- attributes 是一個對象,表示真實 DOM 節(jié)點上所有的屬性礼旅;
- children 對應真實 DOM 的 childNodes膳叨,其中 childNodes 每一項又是類似的結(jié)構(gòu)。
定義好數(shù)據(jù)結(jié)構(gòu)后痘系,現(xiàn)在需要一個可以生成如此結(jié)構(gòu)虛擬 DOM 的方法(類)菲嘴。
用于生產(chǎn)虛擬 DOM:
class VirtualDom {
constructor(tagName, attributes = {}, children = []) {
this.tagName = tagName;
this.attributes = attributes;
this.children = children;
}
}
// 封裝 createVirtualDom 方法,內(nèi)部調(diào)用 Element 構(gòu)造函數(shù)
function createVirtualDom(tagName, attributes, children) {
return new VirtualDom(tagName, attributes, children);
}
上述虛擬 DOM 就可以這樣生成:
const myVirtualDom = createVirtualDom("ul", { id: "ul1" }, [
createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);
生成的虛擬 DOM 對象的數(shù)據(jù)格式更如我們定義的那樣汰翠。
是不是很簡單龄坪?生成了虛擬 DOM 對象后,我們繼續(xù)完成虛擬 DOM 轉(zhuǎn)換為真實 DOM 節(jié)點的過程复唤。
虛擬 DOM 變 真實 DOM
首先創(chuàng)建一個 setAttribute 方法健田,setAttribute 方法的作用是對 DOM 節(jié)點進行屬性設置。
參數(shù)1:DOM 節(jié)點 參數(shù)2:屬性名 參數(shù)3:屬性值
const setAttribute = (node, key, value) => {
switch (key) {
case "style":
node.style.cssText = value;
break;
case "value":
let tagName = node.tagName || "";
tagName = tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
node.value = value;
} else {
// 非 input && textarea 則使用 setAttribute 去設置 value 屬性
node.setAttribute(key, value);
}
break;
default:
node.setAttribute(key, value);
break;
}
};
虛擬 DOM 類中加入 render 實例方法苟穆,該方法的作用是根據(jù)虛擬 DOM 生成真實 DOM 片段:
class VirtualDom {
constructor(tagName, attributes = {}, children = []) {
this.tagName = tagName;
this.attributes = attributes;
this.children = children;
}
render() {
let element = document.createElement(this.tagName);
let attributes = this.attributes;
for (let key in attributes) {
setAttribute(element, key, attributes[key]);
}
let children = this.children;
// 遍歷子節(jié)點抄课, 若 child 也是虛擬節(jié)點,遞歸調(diào)用 render雳旅,若是字符串跟磨,直接創(chuàng)建文本節(jié)點
children.forEach((child) => {
let childElement =
child instanceof VirtualDom
? child.render()
: document.createTextNode(child);
element.appendChild(childElement);
});
return element;
}
}
根據(jù) tagName 創(chuàng)建標簽后,借助工具方法 setAttribute 進行屬性的創(chuàng)建攒盈;
對 children 每一項類型進行判斷抵拘,如果是 VirtualDom 實例,進行遞歸調(diào)用 render 方法型豁;
直到遇見文本節(jié)點類型僵蛛,進行內(nèi)容渲染。
真實 DOM 渲染
有了真實的 DOM 節(jié)點片段迎变,我們趁熱打鐵充尉,將真實的 DOM 節(jié)點渲染到瀏覽器上。
實現(xiàn) renderDOM 方法:
const renderDom = (element, target) => {
target.appendChild(element);
};
截至目前的完整代碼如下:
// 添加屬性方法
const setAttribute = (node, key, value) => {
switch (key) {
case "style":
node.style.cssText = value;
break;
case "value":
let tagName = node.tagName || "";
tagName = tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
default:
node.setAttribute(key, value);
break;
}
};
// 虛擬 DOM 類
class VirtualDom {
constructor(tagName, attributes = {}, children = []) {
this.tagName = tagName;
this.attributes = attributes;
this.children = children;
}
render() {
let element = document.createElement(this.tagName);
let attributes = this.attributes;
for (let key in attributes) {
setAttribute(element, key, attributes[key]);
}
let children = this.children;
children.forEach((child) => {
let childElement =
child instanceof VirtualDom
? child.render()
: document.createTextNode(child);
element.appendChild(childElement);
});
return element;
}
}
// 生成虛擬 DOM 方法
function createVirtualDom(tagName, attributes, children) {
return new VirtualDom(tagName, attributes, children);
}
// 將真實的 DOM 節(jié)點渲染到瀏覽器上
const renderDom = (element, target) => {
target.appendChild(element);
};
// 執(zhí)行方法 生成虛擬 DOM
const myVirtualDom = createVirtualDom("ul", { id: "ul1" }, [
createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);
// 執(zhí)行虛擬 DOM 的 render 方法衣形,將虛擬 DOM 轉(zhuǎn)換為真實 DOM
const realDom = myVirtualDom.render();
// 將真實 DOM 渲染倒瀏覽器上
renderDom(realDom, document.body);
到這就實現(xiàn)了從虛擬 DOM 創(chuàng)建到轉(zhuǎn)換為真實 DOM驼侠,并渲染到瀏覽器上的過程姿鸿,實現(xiàn)起來并不困難。
虛擬 DOM diff
有了上面的實現(xiàn)倒源,可以產(chǎn)出一份虛擬 DOM苛预,并轉(zhuǎn)換為真實 DOM 渲染在瀏覽器中。
虛擬 DOM 也不是一塵不變的笋熬,當用戶在特定操作后热某,會產(chǎn)出一份新的虛擬 DOM,開頭我們也說了虛擬 DOM 的優(yōu)勢之一在于“能夠精確的獲取最小的胳螟、最必要的操作 DOM 的集合”昔馋。
那該如何得出前后兩份虛擬 DOM 的差異,并交給瀏覽器需要更新的結(jié)果呢旺隙? 這就涉及到 DOM diff 的過程绒极。
虛擬 DOM 是個樹形結(jié)構(gòu),所以我們需要對兩份虛擬 DOM 進行遞歸比較蔬捷,將變化存儲在一個變量中:
參數(shù)1:舊的虛擬 DOM 對象 參數(shù)2:新的虛擬 DOM 對象
const diff = (oldVirtualDom, newVirtualDom) => {
let differences = {};
// 遞歸虛擬 DOM 樹垄提,計算差異后結(jié)果放到 differencess 對象中
walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);
// 返回 diff 計算結(jié)果
return differences;
};
walkToDiff 前兩個參數(shù)是兩個需要比較的舊/新虛擬 DOM 對象;第三個參數(shù)記錄 nodeIndex周拐,在刪除節(jié)點時使用铡俐,初始為 0;第四個參數(shù)是一個閉包變量妥粟,記錄 diff 結(jié)果审丘。
waklToDiff 的具體實現(xiàn)為:
let initialIndex = 0;
const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) => {
let diffResult = [];
// 1.如果 newVirtualDom 不存在,說明該節(jié)點被移除勾给,我們將 type 為 REMOVE 的對象放進 diffResult 數(shù)組滩报,并記錄 index
if (!newVirtualDom) {
diffResult.push({
type: "REMOVE",
index,
});
}
// 2.如果新舊節(jié)點都是文本節(jié)點,是字符串
else if (
typeof oldVirtualDom === "string" &&
typeof newVirtualDom === "string"
) {
// 比較文本是否相同播急,如果不同則記錄新的結(jié)果
if (oldVirtualDom !== newVirtualDom) {
diffResult.push({
type: "MODIFY_TEXT",
data: newVirtualDom,
index,
});
}
}
// 3.如果新舊節(jié)點類型相同
else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
// 比較屬性是否相同
let diffAttributeResult = {};
for (let key in oldVirtualDom) {
if (oldVirtualDom[key] !== newVirtualDom[key]) {
// 記錄差異脓钾,直接將 attributes 和 children 進行覆蓋
diffAttributeResult[key] = newVirtualDom[key];
// 處理屬性被刪除的情況
if (key === "attributes") {
// 如果 diffAttributeResult 不含 oldVirtualDom["attributes"] 中的屬性,說明在新的 vdom 中該屬性被刪除
for (let attr in oldVirtualDom["attributes"]) {
if (!diffAttributeResult["attributes"].hasOwnProperty(attr)) {
// 如果屬性被刪除則設置值為空桩警,在渲染時對空值進行判斷即可
diffAttributeResult["attributes"][attr] = "";
}
}
}
}
}
// 舊節(jié)點不存在的新屬性
for (let key in newVirtualDom) {
if (!oldVirtualDom.hasOwnProperty(key)) {
diffAttributeResult[key] = newVirtualDom[key];
}
}
// 如果該層級有差異可训,將差異結(jié)果記錄到 diffResult 數(shù)組中
if (Object.keys(diffAttributeResult).length > 0) {
diffResult.push({
type: "MODIFY_ATTRIBUTES",
diffAttributeResult,
});
}
// 如果有子節(jié)點,遍歷子節(jié)點
oldVirtualDom.children.forEach((child, i) => {
walkToDiff(child, newVirtualDom.children[i], ++initialIndex, differences);
});
}
// 4.else 說明節(jié)點類型不同捶枢,被直接替換了握截,我們直接將新的結(jié)果 push
else {
diffResult.push({
type: "REPLACE",
newVirtualDom,
});
}
// 如果不存在舊節(jié)點
if (!oldVirtualDom) {
diffResult.push({
type: "REPLACE",
newVirtualDom,
});
}
if (diffResult.length) {
differences[index] = diffResult;
}
};
添加 walkToDiff 方法后,整體測試下我們的代碼:
const setAttribute = (node, key, value) => {
switch (key) {
case "style":
node.style.cssText = value;
break;
case "value":
let tagName = node.tagName || "";
tagName = tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
default:
node.setAttribute(key, value);
break;
}
};
class VirtualDom {
constructor(tagName, attributes = {}, children = []) {
this.tagName = tagName;
this.attributes = attributes;
this.children = children;
}
render() {
let element = document.createElement(this.tagName);
let attributes = this.attributes;
for (let key in attributes) {
setAttribute(element, key, attributes[key]);
}
let children = this.children;
children.forEach((child) => {
let childElement =
child instanceof VirtualDom
? child.render()
: document.createTextNode(child);
element.appendChild(childElement);
});
return element;
}
}
function createVirtualDom(tagName, attributes, children) {
return new VirtualDom(tagName, attributes, children);
}
const renderDom = (element, target) => {
target.appendChild(element);
};
const diff = (oldVirtualDom, newVirtualDom) => {
let differences = {};
walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);
return differences;
};
let initialIndex = 0;
const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) => {
let diffResult = [];
// 如果 newVirtualDom 不存在烂叔,說明該節(jié)點被移除谨胞,我們將 type 為 REMOVE 的對象推進 diffResult 變量,并記錄 index
if (!newVirtualDom) {
diffResult.push({
type: "REMOVE",
index,
});
}
// 如果新舊節(jié)點都是文本節(jié)點蒜鸡,是字符串
else if (
typeof oldVirtualDom === "string" &&
typeof newVirtualDom === "string"
) {
if (oldVirtualDom !== newVirtualDom) {
diffResult.push({
type: "MODIFY_TEXT",
data: newVirtualDom,
index,
});
}
}
// 如果新舊節(jié)點類型相同
else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
let diffAttributeResult = {};
for (let key in oldVirtualDom) {
if (oldVirtualDom[key] !== newVirtualDom[key]) {
diffAttributeResult[key] = newVirtualDom[key];
if (key === "attributes") {
for (let attr in oldVirtualDom["attributes"]) {
if (!diffAttributeResult["attributes"].hasOwnProperty(attr)) {
diffAttributeResult["attributes"][attr] = "";
}
}
}
}
}
for (let key in newVirtualDom) {
if (!oldVirtualDom.hasOwnProperty(key)) {
diffAttributeResult[key] = newVirtualDom[key];
}
}
if (Object.keys(diffAttributeResult).length > 0) {
diffResult.push({
type: "MODIFY_ATTRIBUTES",
diffAttributeResult,
});
}
oldVirtualDom.children.forEach((child, index) => {
walkToDiff(
child,
newVirtualDom.children[index],
++initialIndex,
differences
);
});
}
// else 說明節(jié)點類型不同畜眨,被直接替換了昼牛,我們直接將新的結(jié)果 push
else {
diffResult.push({
type: "REPLACE",
newVirtualDom,
});
}
if (!oldVirtualDom) {
diffResult.push({
type: "REPLACE",
newVirtualDom,
});
}
if (diffResult.length) {
console.log(index);
differences[index] = diffResult;
}
};
測試
const myVirtualDom1 = createVirtualDom("ul", { id: "ul1" }, [
createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);
const myVirtualDom2 = createVirtualDom("ul", { id: "ul2" }, [
createVirtualDom("li", { class: "li-stl2" }, ["item4"]),
createVirtualDom("li", { class: "li-stl2" }, ["item5"]),
createVirtualDom("li", { class: "li-stl2" }, ["item6"]),
]);
diff(myVirtualDom1, myVirtualDom2);
得到比較后的結(jié)果數(shù)組
var result = {
"0": [
{
type: "MODIFY_ATTRIBUTES",
diffAttributeResult: {
attributes: { id: "ul2" },
children: [
{
tagName: "li",
attributes: { class: "li-stl2" },
children: ["item4"],
},
{
tagName: "li",
attributes: { class: "li-stl2" },
children: ["item5"],
},
{
tagName: "li",
attributes: { class: "li-stl2" },
children: ["item6"],
},
],
},
},
],
"1": [
{
type: "MODIFY_ATTRIBUTES",
diffAttributeResult: {
attributes: { class: "li-stl2" },
children: ["item4"],
},
},
],
"2": [{ type: "MODIFY_TEXT", data: "item4", index: 2 }],
"3": [
{
type: "MODIFY_ATTRIBUTES",
diffAttributeResult: {
attributes: { class: "li-stl2" },
children: ["item5"],
},
},
],
"4": [{ type: "MODIFY_TEXT", data: "item5", index: 4 }],
"5": [
{
type: "MODIFY_ATTRIBUTES",
diffAttributeResult: {
attributes: { class: "li-stl2" },
children: ["item6"],
},
},
],
"6": [{ type: "MODIFY_TEXT", data: "item6", index: 6 }],
};
測試結(jié)果符合我們的預期术瓮。
此刻我們已經(jīng)通過 diff 方法對兩個虛擬 DOM 進行比對康聂,在 diff 方法中得到差異 differences。得到差異后如何更新視圖呢胞四?
拿到差異 differences恬汁,調(diào)用 patchDiff 方法
const patchDiff = (node, differences) => {
// 用來取差異數(shù)組中每項的索引
// 之所以用對象的形式,是因為在 renderDiff 中被遞歸傳遞辜伟,使用對象可以保證 index 的值不會重復氓侧,如果使用普通的值類型遞歸調(diào)用會出問題
let differ = { index: 0 };
renderDiff(node, differ, differences);
};
patchDiff 方法接收一個真實的 DOM 節(jié)點,它是需要進行更新的 DOM 節(jié)點导狡,同時接收一個差異集合约巷,該集合對接 diff 方法返回的結(jié)果。
在 patchDiff 方法內(nèi)部旱捧,我們調(diào)用了 renderDiff 函數(shù):
const renderDiff = (node, differ, differences) => {
// 獲取差異數(shù)組中的每一項
let currentDiff = differences[differ.index];
// 真實 DOM 節(jié)點
let childNodes = node.childNodes;
// 遞歸調(diào)用自身
childNodes.forEach((child) => {
differ.index++;
renderDiff(child, differ, differences);
});
// 對于當前節(jié)點的差異調(diào)用 updateRealDom 方法進行更新
if (currentDiff) {
updateRealDom(node, currentDiff);
}
};
renderDiff 方法進行自身遞歸独郎,對于當前節(jié)點的差異調(diào)用 updateRealDom 方法進行更新。
updateRealDom 對四種類型的 diff 進行處理:
const updateRealDom = (node, currentDiff) => {
currentDiff.forEach((dif) => {
switch (dif.type) {
case "MODIFY_ATTRIBUTES":
const attributes = dif.diffAttributeResult.attributes;
for (let key in attributes) {
// 不是元素節(jié)點就返回
if (node.nodeType !== 1) return;
const value = attributes[key];
if (value) {
setAttribute(node, key, value);
} else {
// 當 value 為空時枚赡,也就是屬性值在新的虛擬 DOM 中被移除了
node.removeAttribute(key);
}
}
break;
case "MODIFY_TEXT":
node.textContent = dif.data;
break;
case "REPLACE":
let newNode =
dif.newNode instanceof VirtualDom
? render(dif.newNode)
: document.createTextNode(dif.newNode);
node.parentNode.replaceChild(newNode, node);
break;
case "REMOVE":
node.parentNode.removeChild(node);
break;
default:
break;
}
});
};
到這里簡易版的虛擬 DOM 庫就實現(xiàn)了氓癌,下面對代碼進行測試。
完整代碼:
const setAttribute = (node, key, value) => {
switch (key) {
case "style":
node.style.cssText = value;
break;
case "value":
let tagName = node.tagName || "";
tagName = tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
default:
node.setAttribute(key, value);
break;
}
};
class VirtualDom {
constructor(tagName, attributes = {}, children = []) {
this.tagName = tagName;
this.attributes = attributes;
this.children = children;
}
render() {
let element = document.createElement(this.tagName);
let attributes = this.attributes;
for (let key in attributes) {
setAttribute(element, key, attributes[key]);
}
let children = this.children;
children.forEach((child) => {
let childElement =
child instanceof VirtualDom
? child.render() // 若 child 也是虛擬節(jié)點贫橙,遞歸進行
: document.createTextNode(child); // 若是字符串贪婉,直接創(chuàng)建文本節(jié)點
element.appendChild(childElement);
});
return element;
}
}
function createVirtualDom(tagName, attributes, children) {
return new VirtualDom(tagName, attributes, children);
}
const renderDom = (element, target) => {
target.appendChild(element);
};
const diff = (oldVirtualDom, newVirtualDom) => {
let differences = {};
// 遞歸樹 比較后的結(jié)果放到 differences
walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);
return differences;
};
let initialIndex = 0;
const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) => {
let diffResult = [];
// 如果 newVirtualDom 不存在,說明該節(jié)點被移除卢肃,我們將 type 為 REMOVE 的對象推進 diffResult 變量疲迂,并記錄 index
if (!newVirtualDom) {
diffResult.push({
type: "REMOVE",
index,
});
}
// 如果新舊節(jié)點都是文本節(jié)點,是字符串
else if (
typeof oldVirtualDom === "string" &&
typeof newVirtualDom === "string"
) {
// 比較文本是否相同莫湘,如果不同則記錄新的結(jié)果
if (oldVirtualDom !== newVirtualDom) {
diffResult.push({
type: "MODIFY_TEXT",
data: newVirtualDom,
index,
});
}
}
// 如果新舊節(jié)點類型相同
else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
// 比較屬性是否相同
let diffAttributeResult = {};
for (let key in oldVirtualDom) {
if (oldVirtualDom[key] !== newVirtualDom[key]) {
diffAttributeResult[key] = newVirtualDom[key];
if (key === "attributes") {
// 如果 diffAttributeResult 不含 oldVirtualDom["attributes"] 中的屬性尤蒿,說明在新的 vdom 中該屬性被刪除
for (let attr in oldVirtualDom["attributes"]) {
if (!diffAttributeResult["attributes"].hasOwnProperty(attr)) {
diffAttributeResult["attributes"][attr] = "";
}
}
}
}
}
for (let key in newVirtualDom) {
// 舊節(jié)點不存在的新屬性
if (!oldVirtualDom.hasOwnProperty(key)) {
diffAttributeResult[key] = newVirtualDom[key];
}
}
if (Object.keys(diffAttributeResult).length > 0) {
diffResult.push({
type: "MODIFY_ATTRIBUTES",
diffAttributeResult,
});
}
// 如果有子節(jié)點,遍歷子節(jié)點
oldVirtualDom.children.forEach((child, index) => {
walkToDiff(
child,
newVirtualDom.children[index],
++initialIndex,
differences
);
});
}
// else 說明節(jié)點類型不同逊脯,被直接替換了优质,我們直接將新的結(jié)果 push
else {
diffResult.push({
type: "REPLACE",
newVirtualDom,
});
}
if (!oldVirtualDom) {
diffResult.push({
type: "REPLACE",
newVirtualDom,
});
}
if (diffResult.length) {
differences[index] = diffResult;
}
};
const patchDiff = (node, differences) => {
let differ = { index: 0 };
renderDiff(node, differ, differences);
};
const renderDiff = (node, differ, differences) => {
let currentDiff = differences[differ.index];
let childNodes = node.childNodes;
childNodes.forEach((child) => {
differ.index++;
renderDiff(child, differ, differences);
});
if (currentDiff) {
updateRealDom(node, currentDiff);
}
};
const updateRealDom = (node, currentDiff) => {
currentDiff.forEach((dif) => {
switch (dif.type) {
case "MODIFY_ATTRIBUTES":
const attributes = dif.diffAttributeResult.attributes;
for (let key in attributes) {
if (node.nodeType !== 1) return;
const value = attributes[key];
if (value) {
setAttribute(node, key, value);
} else {
node.removeAttribute(key);
}
}
break;
case "MODIFY_TEXT":
node.textContent = dif.data;
break;
case "REPLACE":
let newNode =
dif.newNode instanceof VirtualDom
? render(dif.newNode)
: document.createTextNode(dif.newNode);
node.parentNode.replaceChild(newNode, node);
break;
case "REMOVE":
node.parentNode.removeChild(node);
break;
default:
break;
}
});
};
// 虛擬 DOM1
const myVirtualDom1 = createVirtualDom("ul", { id: "ul1", class: "class1" }, [
createVirtualDom("li", { class: "li-stl1" }, ["item1"]),
createVirtualDom("li", { class: "li-stl1" }, ["item2"]),
createVirtualDom("li", { class: "li-stl1" }, ["item3"]),
]);
// 虛擬 DOM2
const myVirtualDom2 = createVirtualDom(
"ul",
{ id: "ul2", style: "color:pink;" },
[
createVirtualDom("li", { class: "li-stl2" }, ["item4"]),
createVirtualDom("li", { class: "li-stl2" }, ["item5"]),
createVirtualDom("li", { class: "li-stl2" }, ["item6"]),
]
);
將虛擬 DOM 轉(zhuǎn)換為真實 DOM
var element = myVirtualDom1.render();
看到此時的 element 為真實的 DOM 節(jié)點,如下圖所示
將真實 DOM 節(jié)點渲染到瀏覽器上
renderDom(element, document.body);
此時军洼,真實的 DOM 節(jié)點已經(jīng)被渲染到瀏覽器上巩螃,如下圖所示
比較兩個虛擬 DOM 差異
const differences = diff(myVirtualDom1, myVirtualDom2);
得到的差異對象 differences 如下圖所示
分析差異,更新視圖
patchDiff(element, differences);
執(zhí)行后匕争,id 更新為 ul2避乏,并且移除了 class 屬性,添加了 style 屬性甘桑,其余值也修改正確
完結(jié)
盡管缺少了很多細節(jié)優(yōu)化和邊界問題的處理拍皮,但是我們的虛擬 DOM 實現(xiàn)的還是非常強大的歹叮,基本思想和 snabbdom 等一些虛擬 DOM 庫高度一致。