Vue.js設(shè)計與實現(xiàn)之十二-渲染器的核心功能:掛載與更新01
1、寫在前面
在本文中主要講述了如何實現(xiàn)虛擬DOM節(jié)點轉(zhuǎn)成真實DOM樹上,最終掛載到掛載點上。討論了虛擬節(jié)點是如何掛載到DOM樹,又是如何從DOM樹上卸載的,對于屬性又是如何在元素上進(jìn)行設(shè)置的。
2、掛載子節(jié)點和元素的屬性
在上篇文章中,在vnode.children值為字符串時,將其設(shè)置為元素的文本內(nèi)容,當(dāng)vnode.children值為數(shù)組時,表示其有子節(jié)點需要遍歷設(shè)置。我們知道vnode是一個虛擬DOM節(jié)點,vnode.children是一個虛擬DOM樹,其每個元素都是一個虛擬DOM節(jié)點。
const vnode = {
type:"div",
//props是標(biāo)簽的屬性
props:{
id:"super"
},
children:[
{
type:"p",
children:"pingping"
}
]
}
上面的代碼是虛擬DOM樹形結(jié)構(gòu),如果要實現(xiàn)將虛擬VNode轉(zhuǎn)為真實DOM,需要通過掛載和渲染??梢酝ㄟ^mountElement函數(shù)實現(xiàn)節(jié)點的渲染:
function mountElement(vnode, container){
const el = createElement(vnode.type);
//處理children
if(typeof vnode.children === "string"){
// 字符串轉(zhuǎn)為標(biāo)簽的文本內(nèi)容
setElementText(el, vnode.children)
}else if(Array.isArray(vnode.children)){
//虛擬DOM節(jié)點數(shù)組 需要遍歷每個節(jié)點進(jìn)行掛載
vnode.children.forEach(child=>{
patch(null,child,el);
})
}
// 在標(biāo)簽上添加屬性
if(vnode.props){
for(const key in vnode.props){
// 調(diào)用setAttribute在元素上設(shè)置屬性
el.setAttribute(key, vnode.props[key]);
// 也可以使用DOM對象直接設(shè)置屬性
// el[key] = vnode.props[key];
}
}
insert(el, container);
}
在上面代碼片段中,首先會根據(jù)vnode.type的值創(chuàng)建DOM節(jié)點,children的值判斷為string類型時,直接將其設(shè)置為元素的文本內(nèi)容;children的值判斷數(shù)組時,則對數(shù)組內(nèi)的虛擬DOM節(jié)點進(jìn)行遍歷,調(diào)用patch函數(shù)掛載節(jié)點。
在掛載階段patch是沒有舊vnode的,對此傳遞的第一個參數(shù)是null,而在patch函數(shù)執(zhí)行時會遞歸調(diào)用mountElement函數(shù)完成掛載。而patch傳遞的第三個參數(shù)是虛擬節(jié)點要掛載的根節(jié)點,完成掛載后需要給元素遍歷設(shè)置屬性。
3、正確地設(shè)置元素屬性
如何正確地設(shè)置元素屬性,就得先了解HTML Attribute和DOM Properties的差異和關(guān)聯(lián)。我們知道HTML Attribute是定義在HTML標(biāo)簽元素上的屬性,瀏覽器會將其解析創(chuàng)建一個對應(yīng)的DOM對象,這個對象上包含許多屬性(DOM Properties)。
HTML Attribute的作用是設(shè)置與之對應(yīng)的DOM Properties初始值,當(dāng)值發(fā)生改變時,DOM Properties始終存儲著當(dāng)前值,那么通過getAttribute獲取到的也是初始值。
HTML Attribute和DOM Properties。然而在使用Vue.js的單文件模板不會被瀏覽器解析,此時需要框架自己進(jìn)行解析。會影響DOM屬性的添加方式,瀏覽器會解析普通HTML元素代碼,自定分析HTML Attribute設(shè)置合適的DOM Properties。然而在使用Vue.js的單文件模板不會被瀏覽器解析,此時需要框架自己進(jìn)行解析。
那么,看看在Vue.js是如何實現(xiàn)的:
const renderer = createRenderer({
//創(chuàng)建元素
createElement(tag){
return document.createElement(tag);
},
//設(shè)置元素的文本節(jié)點
setElementText(el, text){
el.textContent = text;
},
//用于給指定父節(jié)點添加指定元素
insert(el, parent, achor = null){
parent.insertBefore(el, anchor);
}
// 將屬性設(shè)置相關(guān)操作進(jìn)行封裝,作為渲染器選項進(jìn)行傳值
patchProps(el, key, prevValue, nextValue){
if(shouldSetAsProps(el, key, nextValue)){
const type = typeof el[key];
if(key === "class"){
//采用el.className方式設(shè)置clas,是因為其性能相對于setAttribute和el.classList更高
el.className = nextValue || "";
}else if(type === "boolean" && nextValue === ""){
el[key] = true;
}else{
el[key] = nextValue;
}
}else{
el.setAttribute(key, nextValue);
}
}
})
在上面代碼中,shouldSetAsProps函數(shù)用于分析屬性是否應(yīng)該作為DOM Properties屬性被設(shè)置,返回的是一個布爾值。當(dāng)返回一個true值時,表示應(yīng)該作為DOM Properties被設(shè)置,否則就應(yīng)該使用setAttribute函數(shù)設(shè)置屬性。在設(shè)置屬性時,需要優(yōu)先設(shè)置元素的DOM Properties,當(dāng)其值為空字符串時,需要將其矯正為true。
在mountElement函數(shù)中,只需要調(diào)用patchProps函數(shù)傳遞參數(shù)即可:
function mountElement(vnode, container){
const el = createElement(vnode.type);
//處理children
if(typeof vnode.children === "string"){
// 字符串轉(zhuǎn)為標(biāo)簽的文本內(nèi)容
setElementText(el, vnode.children)
}else if(Array.isArray(vnode.children)){
//虛擬DOM節(jié)點數(shù)組 需要遍歷每個節(jié)點進(jìn)行掛載
vnode.children.forEach(child=>{
patch(null,child,el);
})
}
// 在標(biāo)簽上添加屬性
if(vnode.props){
for(const key in vnode.props){
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container);
}
在上面代碼片段中,mountElement函數(shù)會檢查每個vnode.props中的屬性,調(diào)用patchProps函數(shù)去設(shè)置DOM Properties。
4、卸載操作
在前面兩節(jié)中,討論了如何將虛擬DOM掛載到掛載點上,是通過createRenderer函數(shù)結(jié)合mountElement實現(xiàn)的。而卸載操作發(fā)生在更新階段,即初次掛載完成之后,后續(xù)渲染觸發(fā)的更新。
//初次掛載
renderer.render(vnode, document.querySelector("#app"));
// 再次掛載新vnode,觸發(fā)更新 當(dāng)傳遞的是null,則進(jìn)行卸載之前的操作
renderer.render(vnode, document.querySelector("#app"));
//renderer.render(null, document.querySelector("#app"));
在初次渲染完畢后,后續(xù)渲染時如果傳遞的是null作為新vnode,則表示需要卸載當(dāng)前所有渲染的內(nèi)容。
在上一篇文章中,使用innerHTML設(shè)置為空作為清空容器元素內(nèi)容的方案是存在缺陷的,因為它不會移除綁定在DOM元素上的事件處理函數(shù)。對此,需要先根據(jù)vnode對象獲取到與之關(guān)聯(lián)的真實DOM元素,使用原生DOM操作方法將其進(jìn)行移除。
function unmount(vnode){
const parent = vnode.el.parentNode;
if(parent){
parent.removeChild(vnode.el);
}
}
unmount函數(shù)接收一個虛擬節(jié)點作為參數(shù),并將該節(jié)點對應(yīng)的真實DOM元素從父元素上移除。
注意:在新舊vnode描述內(nèi)容不同時,即vnode.type的屬性不同時,兩個vnode之間就不存在打補丁的意義,此時應(yīng)該使用unmount函數(shù)先將舊元素進(jìn)行卸載,再將n1的值重置為null,最后將新元素進(jìn)行掛載到容器中。
當(dāng)然,即使新舊vnode描述內(nèi)容相同,也要判斷兩者的類型是否相同,vnode可以描述普通標(biāo)簽也可以描述組件,對于不同類型的vnode需要使用不同的掛載或打補丁方式。
function patch(n1, n2, container){
if(n1 && n2.type !== n1.type){
unmount(n1);
n1 = null;
}
const { type } = n2;
if(typeof type === "string"){
if(!n1){
mountElement(n2, container);
}else{
patchElement(n1, n2);
}
}else if(typeof type === "object"){
// n2.type是object對象類型,則描述的是組件
}else if(type === "xxx"){
//處理其他類型vnode
}
}
5、寫在最后
掛載子節(jié)點只需要遞歸調(diào)用patch函數(shù)即可實現(xiàn)掛載,而節(jié)點屬性的設(shè)置就取決于被設(shè)置屬性的特點。在卸載操作時,通過直接使用innerHTML清空容器元素是存在諸多問題的,對此封裝了一個新的卸載函數(shù)unmount。