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