iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Vue.js

淺談vue3源碼,很淺的那種系列 第 15

[Day 15] runtime-core——渲染dom元素

  • 分享至 

  • xImage
  •  

不知不覺間,鐵人賽也過了一半。不過我在寫今天內容的草稿的時間點鐵人賽根本還沒開賽,所以好像也沒甚麼感慨的感覺。

我現在只想著希望正式把這篇文章發出去的時候,我不至於一個讀者都沒有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開始看起。

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元素節點所對應的方式,去決定要比對還是渲染這些子組件以及子組件的子組件。

mountElement

渲染一個元素,除了渲染那個元素本身,還需要把它的子節點也渲染出來:

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元素」


上一篇
[Day 14] runtime-core——比對新舊虛擬dom
下一篇
[Day 16] runtime-core——比對節點
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言