大家好凡纳,我是微微笑的蝸牛,??领铐。
上一篇文章介紹了 jsx 背后的實現(xiàn)悯森,今天來介紹一下 virtual dom。
如何更新 dom
若采用現(xiàn)有的實現(xiàn)方式绪撵,想要更新 dom呐馆,只能先構(gòu)造不同的節(jié)點描述信息,再重新調(diào)用 render 方法莲兢。
先看一個例子,計時器每隔 1s 刷新頁面续膳。
const rootDom = document.getElementById("root");
function tick() {
const time = new Date().toLocaleString();
const clockElement = <h1>{time}</h1>;
SLReact.render(clockElement, rootDom);
}
tick();
setInterval(tick, 1000);
這個例子中改艇,每秒都會調(diào)用一次 render 方法。
而以現(xiàn)有的實現(xiàn)方式坟岔,render 方法中總是在往 dom 樹上添加節(jié)點谒兄。這樣會造成節(jié)點不斷的增加,就像下圖這個樣子社付。
但我們可稍微簡單修改一下 render 的實現(xiàn)承疲,將添加改成替換。
if (!parentDom.lastChild) {
parentDom.appendChild(dom);
} else {
parentDom.replaceChild(dom, parentDom.lastChild);
}
這樣改過之后鸥咖,比上面的實現(xiàn)方案要好一點燕鸽。但是對于復(fù)雜的結(jié)構(gòu)來說,仍是耗費巨大啼辣,因為在操作真實的 dom 樹啊研。
但如果試想有一種方案,只刷新差異部分鸥拧,那么操作 dom 樹的效率將會大大提升党远。
那么如何才能得到前后 dom 樹的差異呢?
這就需要用到中間層富弦,使用額外的結(jié)構(gòu)來存儲真實 dom 樹的結(jié)構(gòu)沟娱。在重新 render 的時候,進行新老對比腕柜,找出差異部分济似,再進行更新矫废。
這種中間結(jié)構(gòu)稱之為 virtual dom,可簡稱 vdom碱屁。為什么叫做虛擬 dom磷脯,因為它只是內(nèi)存中真實 dom 樹的對照。
virtual dom
可想而知娩脾,vdom 與 dom 會有一種映射關(guān)系赵誓。vdom 需要跟真實 dom 進行關(guān)聯(lián),這樣就可以方便的找到真實 dom柿赊,進行操作俩功。那么在 vdom 的信息中就會包含 dom。
此外碰声,vdom 還需包含節(jié)點描述信息诡蜓,不然怎么做對比呢?
vdom 也是樹狀結(jié)構(gòu)胰挑,那么同樣包含子節(jié)點蔓罚。
根據(jù)上述分析,我們便可得到 vdom 的結(jié)構(gòu)瞻颂。它包括節(jié)點描述信息豺谈、關(guān)聯(lián)的真實 dom、子 vdom 節(jié)點贡这。每個 dom 節(jié)點都對應(yīng)著一個 vdom 節(jié)點茬末。
當在做 diff 時,根據(jù)新舊節(jié)點描述信息盖矫,找出差異部分丽惭,盡可能的重用 dom,減少開銷辈双。
那如何來構(gòu)建 vdom 進行 diff 呢责掏?下面我們來一步步的講解。
vdom 結(jié)構(gòu)
虛擬 dom 節(jié)點信息包括三部分:
- 節(jié)點描述信息
- 關(guān)聯(lián)的真實 dom
- 子虛擬 dom 節(jié)點
它的結(jié)構(gòu)如下:
let vdom = { dom, element, childInstances };
根據(jù)之前的 render 方法湃望,其實比較容易改造出這種結(jié)構(gòu)拷橘。因為它返回的是真實 dom,我們只需將返回信息修改為 virtual dom 的結(jié)構(gòu)就好喜爷。
改造過程如下:
- 根據(jù)節(jié)點類型生成真實 dom
- 更新 dom 屬性
- 遞歸處理子節(jié)點
- 獲取子節(jié)點真實 dom冗疮,逐個添加到父節(jié)點
- 返回 virtual dom
代碼如下所示:
// virtual dom,保存真實的 dom檩帐,element术幔,childInstance
function instantitate(element) {
const { type, props } = element;
const isTextElement = type === TEXT_ELEMENT;
// 生成真實 dom
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
// 更新屬性
updateDomProperties(dom, [], props);
// 處理子節(jié)點
const childElements = props.children || [];
// 生成子虛擬 dom
const childInstances = childElements.map(instantitate);
// 獲取子 dom
const childDoms = childInstances.map((childInstance) => childInstance.dom);
// 添加到 dom 樹
childDoms.forEach((childDom) => dom.appendChild(childDom));
// 組成虛擬 dom 結(jié)構(gòu)
const instance = { dom, element, childInstances };
return instance;
}
在得到 virtual dom 結(jié)構(gòu)后,下一步需要做的就是更新真實 dom 節(jié)點湃密。
此時诅挑,render 方法需要進行改造四敞,變?yōu)楸容^前后 vdom 差異。
let rootInstance = null;
function render(element, parentDom) {
const prevInstance = rootInstance;
const nextInstance = reconcile(parentDom, prevInstance, element);
rootInstance = nextInstance;
}
它主要工作是和上一個 virtual dom 實例做對比拔妥,然后進行 dom 樹的更新忿危。
這里我們將 diff 更新的過程叫做 reconcile,它會返回 virtual dom 節(jié)點没龙。如下圖所示:
diff 簡單處理
reconcile 的入?yún)⒂腥齻€铺厨,分別是父 dom 節(jié)點、vdom硬纤、節(jié)點描述信息解滓。
先來看一種簡單的處理方式:
- 如果傳入的 vdom 為空,說明還沒有 dom 節(jié)點筝家,需要將真實 dom 添加到 dom 根節(jié)點上洼裤。
- 如果不為空,則用新的 dom 節(jié)點替換原有 dom 節(jié)點溪王。
如下所示:
function reconcile(parentDom, instance, element) {
if (instance == null) {
// 添加 dom
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else {
// 替換 dom
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
}
}
不知大家發(fā)現(xiàn)了沒腮鞍,上面的處理中,無論哪種條件下都調(diào)用了 instantiate 方法來重新創(chuàng)建 vdom 結(jié)構(gòu)莹菱。
而 instantiate 中會創(chuàng)建真實 dom缕减,這樣在節(jié)點類型沒變化時會產(chǎn)生不必要的開銷。
重用 dom 節(jié)點
這里我們可以稍微優(yōu)化一下芒珠,當節(jié)點類型一樣時,可以不用重新創(chuàng)建 dom 節(jié)點搅裙,復(fù)用已有就行皱卓,然后再更新屬性。
如下所示:
function reconcile(parentDom, instance, element) {
if (instance.element.type === element.type) {
// 復(fù)用 dom部逮,更新屬性
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.element = element;
return instance;
}
}
子節(jié)點 diff
但是娜汁,還存在一個問題,子節(jié)點的 diff 還沒有進行處理兄朋。
在 React 中掐禁,子節(jié)點會有一個額外的屬性 key,以它為標識來對比之前的節(jié)點颅和。
這里傅事,我們將簡單處理,只將每個位置上的子節(jié)點與新的節(jié)點信息進行對比峡扩,進行新增/更新/替換/刪除操作蹭越。
如下所示:
// 對子節(jié)點做處理
function reconcileChildren(instance, element) {
const dom = instance.dom;
// 原有 virutal dom
const childInstances = instance.childInstances;
// 新的節(jié)點描述信息
const nextChildElements = element.props.children || [];
const newChildInstances = [];
// 取最大的,若新子節(jié)點數(shù) < 原節(jié)點數(shù)教届,需移除
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
return newChildInstances;
}
每個位置上的子節(jié)點 diff 仍然會調(diào)用到 reconcile 方法响鹃。
請注意:新舊子節(jié)點的數(shù)目可能是不一樣的驾霜。
若新的子節(jié)點數(shù)目小于舊子節(jié)點數(shù),需要刪除舊子節(jié)點买置。
因為遍歷次數(shù)是由新舊節(jié)點數(shù)目最大的那個決定粪糙。當遍歷次數(shù)超出新子節(jié)點數(shù)時,這時忿项,childElement 為 null蓉冈。
如下圖所示:
對應(yīng)到 reconcile 中的處理,當 element 為空時倦卖,刪除真實 dom 節(jié)點洒擦,然后 vdom 返回 null。
// dom 更新操作
function reconcile(parentDom, instance, element) {
// 省略...
if (element == null) {
console.log("remove dom");
// remove怕膛,若新子節(jié)點數(shù) < 原節(jié)點數(shù)熟嫩,需移除
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type == element.type) {
console.log("reuse dom");
// 重用節(jié)點,更新屬性
updateDomProperties(instance.dom, instance.element.props, element.props);
// 子節(jié)點處理
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
}
}
這樣就完成了 dom diff 的簡單處理褐捻。
完整代碼可查看:https://github.com/silan-liu/slreact/tree/master/part3掸茅。
總結(jié)
這篇文章主要介紹了如何構(gòu)建 virutal dom 結(jié)構(gòu),以及進行簡單的 diff 處理柠逞,以重用 dom 節(jié)點昧狮,減少不必要的開銷。
下一篇將介紹 component 和 state 的實現(xiàn)板壮,敬請期待~