最近剛開始學react,才學三天就想棄了……
class得寫成className、取個dom元素得this.refs.current一堆點、html得寫在函數的return裡,還不能用字串包起來!各種文化衝擊,我知道這不是react的問題,但我寫的好不習慣啊QQ
還是說回vue吧,看得習慣些。
昨天我們把渲染真實dom的方法都寫完了。今天我們要來寫patchElement跟patchChildren,比對新舊節點有哪些子節點需要重新渲染。
先從patchElement說起,因為diff算法在patchChildren裡面,我們把最困難的部分留給明天的我們。patchElement的代碼如下:
const patchElement = (oldNode: VNode, newNode: VNode) => {
const el = newNode.el = oldNode.el
const oldProps = oldNode.props || {}
const newProps = newNode.props || {}
patchProps(oldProps, newProps, <HTMLElement>el)
patchChildren(oldNode, newNode, <HTMLElement>el)
}
VNode.el指向虛擬節點在真實dom對應的節點,新舊虛擬節點所對應的真實節點都是同一個,所以const el = newNode.el = oldNode.el。
然後patchProps比對一下這兩個節點的屬性一不一樣:
const patchProps = (oldProps: any, newProps: any, el: HTMLElement) => {
for (let key in newProps) {
hostPatchProp(el, key, oldProps[key], newProps[key])
}
for (let key in oldProps) {
if (newProps[key] == null) {
hostPatchProp(el, key, oldProps[key], null)
}
}
}
把新虛擬節點多的屬性給真實dom節點補上去,少的拔掉就完事了。
比對完屬性,就是比對子節點的環節了。
比對子節點,可以根據子節點是空的(沒有子節點)、文本或陣列(真的有一個以上的dom元素節點),分成以下六種情況考慮:
寫成代碼就會是這樣:
const patchChildren = (oldNode: VNode, newNode: VNode, el: HTMLElement) => {
const oldChildren = oldNode.children
const newChildren = newNode.children
const oldShapeFlag = oldNode.shapeFlag
const newShapeFlag = newNode.shapeFlag
if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 舊子節點為陣列;新子節點為文字 -> 刪除舊子節點
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) unmountChildren(<Array<VNode>>oldChildren)
// 舊子節點為文字或空;新子節點為文字
if (oldChildren !== newChildren) hostSetElementText(el, <string>newChildren)
} else {
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// diff
patchKeyedChildren(<Array<VNode>>oldChildren, <Array<VNode>>newChildren, el)
} else {
// 舊子節點為陣列,新子節點為空
unmountChildren(<Array<VNode>>oldChildren)
}
} else {
// 舊子節點為文本或空,新節點為陣列或空
if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) hostSetElementText(el, '')
if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) mountChildren(<Array<VNode>>newChildren, el)
}
}
}
const unmountChildren = (children: Array<VNode>) => {
children.forEach(child => {
unmount(child)
})
}
最下面的unmountChildren沒甚麼好說的,就是一個把所有子節點全部幹掉的方法,就不多做贅述。
我們可以先看if(newShapeFlag & ShapeFlags.TEXT_CHILDREN)裡面的部分,也就是如果新節點是文本的情況:
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) unmountChildren(<Array<VNode>>oldChildren)
if (oldChildren !== newChildren) hostSetElementText(el, <string>newChildren)
上面的如果舊節點是陣列就把他幹掉很好理解,值得一提的是下面的if (oldChildren !== newChildren)。
如果舊節點是陣列,那它會在上面那個if被幹掉,也就是說進入下面的if時,舊節點已經只可能是空或文本節點。無論是空還是文本節點,我們都能用hostSetElementText,將真實dom的textContent置換成新節點的內文。
接著看else的部分,也就是新節點的文本不是文本的情況。
首先考慮新舊節點是否都是陣列,當新舊節點都是陣列,我們就得用diff算法來比對新舊節點的子節點,刪除哪些、渲染哪些能用最低的效能消耗實現數據更新驅動視圖。這部分我預計明天留一天完整的篇幅來講,我們這邊先跳過。
然後是當新子節點是空,舊子節點是陣列的情況:
if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) // ......
else {
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// diff先跳過
} else {
// 舊子節點為陣列,新子節點為空
unmountChildren(<Array<VNode>>oldChildren)
}
}
}
當新子節點為空,除了舊子節點也是空的情況可以甚麼事都不做以外,一律都是把真實dom的子節點全幹掉,因此unmountChildren()。
最後當新子節點是陣列或空,舊子節點是文本時:
if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) hostSetElementText(el, '')
if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) mountChildren(<Array<VNode>>newChildren, el)
無論新子節點是甚麼,總之舊的文本都是該幹掉的。
幹掉以後再判斷新子節點是不是陣列,只要是陣列而不是空,就把他們渲染上去。
其實runtime-core的renderer.ts看上去代碼又多又複雜,本質就是根據不同的情況做不同的事。除去if else判斷多也沒剩甚麼,實際上並不難。核心理念就是舊節點多的幹掉,新節點多的渲染,比完節點比兒子,比完兒子比孫子,把這句話翻譯成js就是這幾天我們所學的東西了——除了我們唯獨還沒動工的diff算法。
明天就讓我們來見識vue源碼最著名的算法,diff算法的精妙。
githubmain分支commit「[Day 16] runtime-core——比對節點」