iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Vue.js

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

[Day 16] runtime-core——比對節點

  • 分享至 

  • xImage
  •  

最近剛開始學react,才學三天就想棄了……
class得寫成className、取個dom元素得this.refs.current一堆點、html得寫在函數的return裡,還不能用字串包起來!各種文化衝擊,我知道這不是react的問題,但我寫的好不習慣啊QQ

還是說回vue吧,看得習慣些。

昨天我們把渲染真實dom的方法都寫完了。今天我們要來寫patchElement跟patchChildren,比對新舊節點有哪些子節點需要重新渲染。

patchElement

先從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節點補上去,少的拔掉就完事了。
比對完屬性,就是比對子節點的環節了。

patchChildren

比對子節點,可以根據子節點是空的(沒有子節點)、文本或陣列(真的有一個以上的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——比對節點」


上一篇
[Day 15] runtime-core——渲染dom元素
下一篇
[Day 17] runtime-core——diff算法
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言