不知不覺間,鐵人賽也過了一半。不過我在寫今天內容的草稿的時間點鐵人賽根本還沒開賽,所以好像也沒甚麼感慨的感覺。
我現在只想著希望正式把這篇文章發出去的時候,我不至於一個讀者都沒有QwQ
今天我們要針對新節點是文本、Fragment、dom元素節點的元素三種情況去寫三種對應的方法。首先先來寫文本的吧:
const processText = (oldNode: VNode, newNode: VNode, container: HTMLElement) => {
if (oldNode === null) {
newNode.el = hostCreateText(<string>newNode.children)
hostInsert(newNode.el, container)
} else {
const el = newNode.el = oldNode.el
if (oldNode.children !== newNode.children) hostSetText(el, <string>newNode.children)
}
}
嗯還真沒甚麼重點,跟/src/runtime-dom/nodeOps.ts對照一下就明白了,都是些dom元素的簡單操作,把新的文本寫入節點就完事了。
接下來是Fragment的情況:
const processFragment = (oldNode: VNode, newNode: VNode, container: HTMLElement) => {
if (oldNode === null) mountChildren(<(VNode | string)[]>newNode.children, container)
else patchChildren(oldNode, newNode, container)
}
Fragment是虛構的標籤,它並不需要被渲染,因此只要遍歷比對新舊節點的子節點是否相同,然後把不同的部分重新渲染即可。這邊提到遍歷比對子節點的mountChildren跟patchChildren先放著不管,之後再回來寫。
再來是dom元素節點的情況:
const processElement = (oldNode: VNode, newNode: VNode, container: HTMLElement, anchor: HTMLElement | Text) => {
if (oldNode === null) mountElement(newNode, container, anchor)
else patchElement(oldNode, newNode)
}
如果沒有舊節點,便無需比對,直接把新節點渲染上去即可;要是有舊節點,則需要去比對新舊節點。這邊的mountElement和patchElement同樣先放著不管,待會回來寫。
順帶一提,目前我們接收到的形參anchor都還是null,之後遞歸調用patch方法比對子節點時才會傳值進來,讓子節點能被HTMLElement.insertBefore插入到正確的位置。
好的,目前我們定義了針對文本、Fragment、dom元素節點三種情況的處理方式,只差寫出能夠比對及渲染dom元素的mountChildren、patchChildren、mountElement以及patchElement四種方法,讓我們來一個個完成它們。
先從最簡單的mountChildren開始看起。
渲染子節點,其實就只是遍歷每個子節點,然後一一渲染它們:
const mountChildren = (children: (VNode | string)[], container: HTMLElement) => {
for (let i = 0; i < children.length; i++) {
children[i] = normalize(children[i])
patch(null, <VNode>children[i], container)
}
}
// 將可能是字串的子節點加工為虛擬DOM
const normalize = (child: VNode | string) => {
if (typeof child === 'string') return createVNode(Text, null, <string>child)
return <VNode>child
}
考慮到子節點中可能混有尚未被加工成虛擬dom節點的純字串文字,因此先將其正規化(normalize),確保子節點中的每一項都必須是虛擬dom節點。
之後再遞歸調用patch方法,重新依文本節點或dom元素節點所對應的方式,去決定要比對還是渲染這些子組件以及子組件的子組件。
渲染一個元素,除了渲染那個元素本身,還需要把它的子節點也渲染出來:
const mountElement = (vnode: VNode, container: HTMLElement, anchor: HTMLElement | Text) => {
const { type, props, children, shapeFlag } = vnode
const el = vnode.el = hostCreateElement(<string>type)
if (props) {
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) hostSetElementText(el, <string>children)
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(<VNode[]>children, el)
}
hostInsert(el, container, anchor)
}
所以首先我們需要創建一個dom元素,這沒甚麼好說的:
const el = vnode.el = hostCreateElement(<string>type)
然後如果這個dom元素有class、style或任何屬性,我們也需要給創建出來的dom元素加上去:
if (props) {
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
接著我們要判斷這個節點的子節點是文本,還是有更多dom元素節點。若是前者,僅需將文本內容寫進textContent即可;若是後者,則需再調用先前寫好的mountChildren方法,遍歷渲染它的所有子節點:
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) hostSetElementText(el, <string>children)
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(VNode[]>children, el)
}
最後只要再將我們創建,並且遍歷渲染完所有子節點的dom元素插入到container之中即可。
hostInsert(el, container, anchor)
至於patchElement和patchChildren包含vue3源碼中最有名的diff算法,講起來最花時間,所以今天就先斷在這裡,目前暫且知道這兩個方法會比對新舊節點(或它的每一個子節點)有哪些不同,哪裡需要重新渲染即可。
githubmain分支commit「[Day 15] runtime-core——渲染dom元素」