vue3 diff 算法详解

115 阅读2分钟

vue3 diff 算法详解

源码地址

源码地址:patchChildren函数

补充说明

  • patchFlag:patchFlag是在编译template模板时,给vnode添加的一个标识信息,这个标识信息反映了vnode的哪些部位绑定了动态值,这样在更新阶段,减少非动态内容的对比消耗
  • patchFlag的类型
    export const enum PatchFlags { 
    // 表示vnode具有动态textContent的元素 
    TEXT = 1, 
    // 表示vnode具有动态的class 
    CLASS = 1 << 1, 
    // 表示具有动态的style 
    STYLE = 1 << 2, 
    // 表示具有动态的非class和style的props 
    PROPS = 1 << 3, 
    // 表示props具有动态的key,与CLASS、STYLE、PROPS冲突 
    FULL_PROPS = 1 << 4, 
    // 表示有监听事件(在同构期间需要添加) 
    HYDRATE_EVENTS = 1 << 5, 
    // 表示vnode是个children顺序不会改变的fragment 
    STABLE_FRAGMENT = 1 << 6,
    // 表示children带有key的fragment 
    KEYED_FRAGMENT = 1 << 7, 
    // 表示children没有key的fragment
    UNKEYED_FRAGMENT = 1 << 8, 
    // 表示vnode只需要非props的patch。例如只有标签中只有ref或指令 
    NEED_PATCH = 1 << 9,
    // 表示vnode存在动态的插槽。例如动态的插槽名 
    DYNAMIC_SLOTS = 1 << 10, 
    // 表示用户在模板的根级别存在注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离 DEV_ROOT_FRAGMENT = 1 << 11, 
    // 以下都是一些特殊的flag,它们不能使用位运算进行匹配 
    // 表示vnode经过静态提升
    HOISTED = -1,
    // diff算法应该退出优化模式 
    BAIL = -2 
    }
    

核心方法

  • patchChildren

        /* 用来比对更新子节点,当节点的标记(patchFlag)为KEYED_FRAGMENT或UNKEYED_FRAGMENT
        则会分别调用patchKeyedChildren和patchUnkeyedChildren进行比对 */
       const patchChildren:PatchChildrenFn = (n1,n2,...) => {
           // n1 旧节点 n2 新节点
           const c1 = n1 && n1.children
           const prevShapeFlag = n1 ? n1.shapeFlag : 0
           const c2 = n2.children
    
           const { patchFlag, shapeFlag } = n2
           if (patchFlag > 0) {
               // 通过位、与来判断
               if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
                   // 携带key值的情况下,新旧子节点列表的比对,利用到了diff算法。
                   patchKeyedChildren(n1,n2,...) return
               } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
                   // 没有携带key
                   patchUnkeyedChildren(n1,n2,...) return
               }
           }
           ...
       }
    
  • isSameVNodeType

        function isSameVNodeType(n1,n2):boolean {
            // 根据节点的类型和key值来快速判断两个VNode是否相同
            return n1.type === n2.type && n1.key === n2.key 
        }
    
  • patchKeyedChildren

    执行步骤

        const patchKeyedChildren = (c1,c2,...) => {
            let i = 0
            const l2 = c2.length // 新节点的长度
            let e1 = c1.length - 1 // 旧节点最后一个元素的位置
            let e2 = l2 - 1 // 新节点最后一个元素的位置
            // 1. 从头部对比
            while(i<=el&&i<=e2){...}
            // 2. 从尾部对比
            while(i<=el&&i<=e2){...}
            // 3. 挂载新的节点
            if(i>e1){...}
            // 4. 卸载新的节点
            else if(i>e2){...}
            // 5. 对比剩余部分 寻找是否存在公用节点并移动位置
            else{...}
        }
    

    代码调试

    <script src="../../dist/vue.global.js"></script>
    
    <div id="demo">
      <ul>
        <li v-for="item in list" :key="item">{{item}}</li>
      </ul>
      <button @click="handleChange">change</button>
    </div>
    
    <script>
      const App = Vue.defineComponent({
        // render() {
        //   return Vue.h('div', 'foo')
        // }
        data() {
          return {
            list: ['a', 'b', 'c']
          }
        },
        methods: {
          handleChange() {
            this.list = ['a', 'b', 'c', 'd']
          }
        }
      })
      Vue.createApp(App).mount('#demo')
    </script>
    

    头比较

        while (i <= e1 && i <= e2) {
          const n1 = c1[i]
          const n2 = (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i])) // 验证和处理VNode
          if (isSameVNodeType(n1, n2)) { // 验证节点是否一致
            patch(...)
          } else {
            break // 不一致则跳出循环 开始尾循环
          }
          i++ // 索引向右走
        }
    

    尾比较

        while (i <= e1 && i <= e2) {
          const n1 = c1[e1] // 最后一位的旧节点
          const n2 = (c2[e2] = optimized
            ? cloneIfMounted(c2[e2] as VNode)
            : normalizeVNode(c2[e2]))
          if (isSameVNodeType(n1, n2)) {
            patch(...)
          } else {
            break
          }
          e1-- // 旧节点长度减小
          e2-- // 新节点长度减小
        }
    

    新增处理

    经过头和尾 能够得到 i(能够复用的vnode的位置) e1(旧节点匹配的最后一位索引) e2(新节点匹配的最后一位索引)

      ```js
     // i > e1 && i <= e2,即c1旧节点列表遍历完了,c2新节点列表还未遍历完。此时c2剩余未遍历的节点,
     // 即全部都是新节点,直接继续遍历通过patch打补丁完成挂载(mount)即可
          if(i>e1){
              if(i<=e2){
              const nextPos = e2 + 1 // 让nextPos指向e2指向的下一个节点
              const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
              while (i <= e2) {
                      patch(...) i++ 
                      }
                  }
              }
      ```
      
    

    image.png

    删除处理

        //i <= e1 && i > e2,此时c2新节点列表已经遍历完成,但c1未遍历完,
        //即旧节点列表c1里的所有节点都是没用的,直接继续遍历卸载掉(unmounted)c1剩余节点即可
        else if(i>e2){
            while(i<=e1){
                unmount(...) i++
            }
        }
    

image.png

处理剩余中间部分 寻找可复用node并移动

  1. 处理新节点的剩余部分[s2,e2],将其转换 为keyToNewIndexMap:key:index的map结构
  2. 根据toBePatched([s2,e2]的长度)生成一个newIndexToOldIndexMap值都为0的数组
  3. 遍历旧节点(s1,e2) 寻找新节点的值在keyToNewIndexMap中的索引(newIndex)不存在则为undefined(后面执行unmount操作),存在则完善newIndexToOldIndexMap中的数据(主要用于第二个move循环) 执行patch操作
  4. 如果newIndex大于maxNewIndexSoFar(当前可以复用节点的最大索引)则说明需要进行move操作 求newIndexToOldIndexMap的最长稳定子序列
  • 若该节点在newIndexToOldIndexMap中的值为0,则说明为新增节点,对其新增
  • 否则遍历newIndexToOldIndexMap,若索引值在increasingNewIndexSequence中, 则跳过该节点不移动,否则移动节点

image.png

    const s1 = i // prev starting index s1指向旧节点开始的索引(剩余中间部分)
      const s2 = i // next starting index s2指向新节点开始的索引(剩余中间部分)

      // 5.1 build key:index map for newChildren 
      //生成新节点 key:index 的映射关系 方便后面查找可复用节点
      const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
      for (i = s2; i <= e2; i++) {
        const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
        if (nextChild.key != null) {
          if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
            warn(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            )
          }
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

      /* 5.2 loop through old children left to be patched and try to patch
      matching nodes & remove nodes that are no longer present */
      // newIndexToOldIndexMap的索引index即c2当前项的位置,value为c2项在c1中的位置加1。
      //如果当前位置的值为0,则c2该位置的节点是全新的节点,直接生成即可 。否则,通过移动来实现复用
      let j
      let patched = 0
      const toBePatched = e2 - s2 + 1 // 5 - 2 + 1 中间待修补的范围区间长度 [s2,e2]
      let moved = false
      // used to track whether any node has moved
      let maxNewIndexSoFar = 0
      // works as Map<newIndex, oldIndex>
      // Note that oldIndex is offset by +1
      // and oldIndex = 0 is a special value indicating the new node has
      // no corresponding old node.
      // used for determining longest stable subsequence
      const newIndexToOldIndexMap = new Array(toBePatched) // 一个对应toBePatched长度且内容为0的数组
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
      // newIndexToOldIndexMap:[0,0,0,0]

      // 利用c1[s1]的key值,在keyToNewIndexMap中找c2同key值项的位置赋值给newIndex,
      //找到则完善newIndexToOldIndexMap,否则unmount卸载掉当前项
      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i]
        if (patched >= toBePatched) {
          // all new children have been patched so this can only be a removal
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex // prevChild.key 在newIndexToOldIndexMap不存在则为undefined
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
          // key-less node, try to locate a key-less node of the same type
          for (j = s2; j <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
              newIndex = j
              break
            }
          }
        }
        // 如果 newIndex 为undefined 说明新节点中没有 则执行卸载操作
        if (newIndex === undefined) {
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          // 将newIndexToOldIndexMap中可复用的节点位置 赋值为i+1
          newIndexToOldIndexMap[newIndex - s2] = i + 1

          // maxNewIndexSoFar 当前可以复用节点的最大索引
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            moved = true
          }
          console.log('prevChild => ', prevChild)
          console.log('c2[newIndex] => ', c2[newIndex])

          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          patched++
        }
      }

      // 5.3 move and mount
      // generate longest stable subsequence only when nodes have moved
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      // looping backwards so that we can use last patched node as anchor
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
          // mount new
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (moved) {
          // move if:
          // There is no stable subsequence (e.g. a reverse)
          // OR current node is not among the stable sequence
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }
  • patchUnkeyedChildren

    用于比对两个没有添加key的新旧Vnode列表

    1. 获取旧节点列表c1和新节点列表c2的长度,再取其最小长度(防止遍历的时候越界)
        c1 = c1 || EMPTY_ARR
        c2 = c2 || EMPTY_ARR
        const oldLength = c1.length
        const newLength = c2.length
        const commonLength = Math.min(oldLength, newLength)
    
    1. 头头比对,一个个patch,相同则复用,不同则直接打补丁、重新生成再替换。
        let i
        for (i = 0; i < commonLength; i++) {
          const nextChild = c2[i]
          patch(
            c1[i],
            nextChild,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
    
    1. 如果c2遍历完,c1未遍历完,即旧节点列表未遍历完,卸载掉多余的节点。相反,新节点列表未遍历完,则生成新的节点。
         if (oldLength > newLength) {
          // remove old
          unmountChildren(
            c1,
            parentComponent,
            parentSuspense,
            true,
            false,
            commonLength
          )
        } else {
          // mount new
          mountChildren(
            c2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            commonLength
          )
        }