最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue3核心组件更新diff算法(简易)

    正文概述 掘金(小桂summer)   2021-03-07   692

    前言

    • 数据发生变化时 组件更新
    • 新老虚拟节点 进行比对
    • 最长递增子序列
    • 主要通过patch(老虚拟节点, 新虚拟节点, 父元素, 参照物)方法比对
    • 有以下几种情况
      1. 类型不同直接替换
      2. 比对属性
      3. 老的是数组 新的是文本(孩子)
      4. 都是文本
      5. null 特殊情况 删除掉老的
      6. 老的是文本 现在是数组(孩子)
      7. 新的是数组 老的也是数组(孩子 重点讲diff算法)
    • 新的是数组 老的也是数组有以下几种情况
      1. diff children 1 从头开始比 不同的就停止
      2. diff children 2 从尾开始比 不同的就停止
      3. diff children 3 老的少 新的多(有一方比对完成)
      4. diff children 4 老的多 新的少(有一方比对完成)
      5. diff children 5 乱序比较

    示例

    <script src="../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js"></script>
    <div id="app"></div>
    <script>
    const { createApp, h, reactive, ref } = VueRuntimeDOM
    
    let App = {
      setup(props, context) {
        let flag = ref(true)
        setTimeout(() => { flag.value = false }, 2000)
    
        return () => {
          // 1.type 不同直接替换
          // return flag.value ?
          //   h('div', { style: { color: '#000' } }, 'Bob') :
          //   h('p', { style: { color: 'blue' } }, 'Tom')
    
          // 2.比对属性
          // return flag.value ?
          //   h('div', { style: { color: '#000' } }, 'Bob') :
          //   h('div', { style: { color: 'blue' } }, 'Tom')
          
          // 3.老的是数组 新的是文本
          // 4.都是文本
          // 5.null 特殊情况 删除掉老的
          // 6.老的是文本 现在是数组
          // return flag.value ? h('div', null, [h('span', null, 'Bob')]) : h('div', null, 'Tom')
          // return flag.value ? h('div', null, 'Bob') : h('div', null, 'Tom')
          // return flag.value ? h('div', null, [h('span', null, 'Bob')]) : h('div', null)
          // return flag.value ? h('div', null, 'Bob') : h('div', null)
    
    
          // diff children 1 从头开始比 不同的就停止
          // diff children 2 从尾开始比 不同的就停止
          // diff children 3 老的少 新的多(有一方比对完成)
          // diff children 4 老的多 新的少(有一方比对完成)
          // diff children 5 乱序比较
          return flag.value ?
            h('div', { style: { color: '#000' } }, [
                h('li', { key: 'a' }, 'a'),
                h('li', { key: 'b' }, 'b'),
                h('li', { key: 'c' }, 'c'),
                h('li', { key: 'd', style: {color: 'green'}}, 'd'),
                h('li', { key: 'e' }, 'e'),
                // h('li', { key: 'q' }, 'q'),
                h('li', { key: 'f' }, 'f'),
                h('li', { key: 'g' }, 'g'),
            ]) :
            h('div', { style: { color: 'blue' } }, [
                h('li', { key: 'a' }, 'a'),
                h('li', { key: 'b' }, 'b'),
                h('li', { key: 'e' }, 'e'),
                h('li', { key: 'c' }, 'c'),
                h('li', { key: 'd', style: {color: 'red'}}, 'd'),
                h('li', { key: 'h' }, 'h'),
                h('li', { key: 'f' }, 'f'),
                h('li', { key: 'g' }, 'g'),
            ])
        }
      },
    }
    
    let app = createApp(App, { name: 'zf', age: 12 })
    app.mount('#app')
    </script>
    

    组件更新

    /**
       * @desc 创建组件的effect函数 执行render函数
       */
      const setupRenderEffect = (instance, container) => {
        instance.update = effect(function componentEffect() {
          if (!instance.isMounted) {
            let proxyToUse = instance.proxy
            let subTree = instance.subTree = instance.render.call(proxyToUse, proxyToUse)
    
            // 用render函数的返回值 继续渲染
            patch(null, subTree, container)
            instance.isMounted = true
          } else {
            // 看这里
            // 获取老组件的subTree(组件render方法返回的vnode) 在调用一次render函数 获取新的subTree
            // 最后patch比对
            const prevTree = instance.subTree
            let proxyToUse = instance.proxy
            const nextTree = instance.render.call(proxyToUse, proxyToUse)
    
            instance.subTree = nextTree
            patch(prevTree, nextTree, container)
          }
        }, {
          scheduler: queueJob
        })
    
      }
    

    新老元素不一样

    /**
       * @param n1          oldVNode
       * @param n2          newVNode
       * @param container   Dom
       * @param anchor      参照物节点
       */
      /**
       * @desc 比较两个虚拟节点 是否一致
       */
      const isSameVNodeType = (n1, n2) => {
        return n1.type === n2.type && n1.key === n2.key
      }
    
      /**
       * @desc 销毁元素, 组件
       */
      const unmount = (n1) => {
        // 源码中如果是组件(这里就展示)
        // 调用的组件的生命周期 调用组件的销毁方法等
        hostRemove(n1.el)
      }
    
      const patch = (n1, n2, container, anchor = null) => {
        const { shapeFlag, type } = n2
    
        // 看这里
        // 1.不同直接替换 将新的虚拟节点渲染成真实DOM
        if (n1 && !isSameVNodeType(n1, n2)) {
          anchor = hostNextSibling(n1.el)
          // 直接移除老节点
          unmount(n1)
          n1 = null
        }
    
        switch (type) {
          case Text:
            processText(n1, n2, container)
            break;
        
          default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
              // 渲染成真实DOM 挂载页面
              processElement(n1, n2, container, anchor)
            } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
              processComponent(n1, n2, container)
            }
    
            break;
        }
      }
    

    元素一样

    • 如果前后虚拟节点一样 将老的dom元素 复用
    • 然后比对属性 和 孩子(子节点)
    /**
     * @desc 比对元素节点
     */
    const patchElement = (n1, n2, container) => {
      // 相同节点
      let el = (n2.el = n1.el)
    
      // 2.比对属性
      const oldProps = n1.props || {}
      const newProps = n2.props || {}
      patchProps(oldProps, newProps, el)
    
      // 比对孩子
      patchChildren(n1, n2, el)
    }
    

    比对属性

    /**
     * @desc 比对属性
     */
    const patchProps = (oldProps, newProps, el) => {
      if (oldProps !== newProps) {
        for (let key in newProps) {
          const prev = oldProps[key]
          const next = newProps[key]
          if (prev !== next) {
            hostPatchProp(el, key, prev, next)
          }
        }
        for (const key in oldProps) {
          if (!(key in newProps)) {
            hostPatchProp(el, key, oldProps[key], null)
          }
        }
      }
    }
    

    比对孩子(子节点)

    /**
       * @desc 比对孩子
       */
      const patchChildren = (n1, n2, el) => {
        const c1 = n1.children
        const c2 = n2.children
    
        const prevShapeFlag = n1.shapeFlag
        const shapeFlag = n2.shapeFlag
    
        // c2如果是文本
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          // 3.老的是数组 新的是文本 销毁老的
          if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            unmountChildren(c1)
          }
    
          // 4.都是文本
          if (c1 !== c2) {
            hostSetElementText(el, c2)
          }
    
        } else {
          // c2否则是null 或者 数组
          if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
              // 都是数组
              // 核心diff
              patchKeyedChildren(c1, c2, el)
            } else {
              // 5.null 特殊情况 删除掉老的
              unmountChildren(c1)
            }
    
          } else {
            // 6.老的是文本 现在是数组
            if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
              hostSetElementText(el, '');
            }
    
            if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
              mountChildren(c2, el);
            }
    
          }
    
        }
    
        
      }
    

    比对孩子(子节点) 核心diff算法

    • 孩子 都是数组 进行比对
    • vue3采用从头开始比较,两个尾指针
    • vue2采用的是双指针算法

    从头开始(sync form start)

    vue3核心组件更新diff算法(简易)
    /**
     * @desc 核心diff 比对孩子
     * @param c1 老的孩子
     * @param c2 新的孩子
     * @param el 父元素
     */
    const patchKeyedChildren = (c1, c2, el) => {
      // Vue3都是默认从头开始比对
      let i = 0
      let e1 = c1.length - 1
      let e2 = c2.length - 1
    
      // diff children 1 从头开始比 不同的就停止
      while (i <= e1 && i <= e2) {
        const n1 = c1[i]
        const n2 = c2[i]
        if (isSameVNodeType(n1, n2)) {
          patch(n1, n2, el)
        } else {
          break;
        }
    
        i++;
      }
    
      // ...
    }
    

    从尾开始(sync from end)

    vue3核心组件更新diff算法(简易)
    const patchKeyedChildren = (c1, c2, el) => {
      // ...
    
      // diff children 2 从尾开始比 不同的就停止
      while (i <= e1 && i <= e2) {
        const n1 = c1[e1]
        const n2 = c2[e2]
        if (isSameVNodeType(n1, n2)) {
            patch(n1, n2, el)
        } else {
            break;
        }
        e1--;
        e2--;
      }
    
      // ...
    
    }
    

    老的少 新的多(common sequence + mount)

    • 前提是有一方 已经结束
    • 可能是在后面添加 也可能是在前面添加
    • 判断有没有下一个节点 加在下一个节点前面
    • 没有就是appendchild往后添加
    vue3核心组件更新diff算法(简易) vue3核心组件更新diff算法(简易)
    const patchKeyedChildren = (c1, c2, el) => {
      // ...
      
      if (i > e1) {
        // diff children 3 老的少 新的多(有一方比对完成)
        // 两种情况从前往后比时 和 从后往前比时(同一个逻辑)
        if (i <= e2) {
          // 想知道是向前插入 还是向后插入
          const nextPos = e2 + 1
          const anchor = nextPos < c2.length ? c2[nextPos].el : null
          while (i <= e2) {
            patch(null, c2[i], el, anchor)
            i++;
          }
        }
      }
    
      // ...
    }
    

    老的多 新的少(common sequence + unmount)

    • 前提是有一方 已经结束
    • 直接删除老的节点
    vue3核心组件更新diff算法(简易) vue3核心组件更新diff算法(简易)
    const patchKeyedChildren = (c1, c2, el) => {
      // ...
    
      if (i > e1) {
      } else if (i > e2) {
    
       // diff children 4 老的多 新的少(有一方比对完成)
        while (i <= e1) {
          unmount(c1[i])
          i++;
        }
    
      } else {}
    
      // ...
    }
    

    乱序比较(unknown sequence)

    步骤一

    • 创建 新的虚拟dom{key => index} 映射表
    • 在新的里面去寻找老的key
    • 没有直接删除
    • 如果在新的里面找到 进行比对
    • 问题:
    • 位置没有移动, 只是把当前已有的进行了比对
    vue3核心组件更新diff算法(简易)
    const patchKeyedChildren = (c1, c2, el) => {
      // ...
    
      if (i > e1) {
      } else if (i > e2) {
      } else {
        // diff children 5 乱序比较
        // 用新的元素做成一个映射表去老的里找
        // 一样的就复用 不一样的要不插入 要不删除
        let s1 = i
        let s2 = i
    
        // 用的是新的做的映射表
        const keyToNewIndexMap = new Map()
    
        // 结果如: { e=>2, c=>3, d=>4, h=>5 }
        for (let i = s2; i <= e2; i++) {
          const childVNode = c2[i]
          keyToNewIndexMap.set(childVNode.key, i)
        }
    
        for (let i = s1; i<=e1 ; i++) {
          const oldVnode = c1[i]
          let newIndex = keyToNewIndexMap.get(oldVnode.key)
          if(newIndex === undefined) {
            // 新的没有 删除老的
            unmount(oldVnode)
          } else {
            // 在老的中找到 做了新老虚拟比较 但是位置没有移动
            patch(oldVnode, c2[newIndex], el)
          }
        }
    
    
      }
    
      // ...
    }
    

    步骤二

    • 插入老的不存在的节点
    • 首先创建一个新的索引对应老的位置的索引数组
    • 从后往前插入方法 将节点一一插入
    • 问题:
    • 有些节点的位置是没必要移动的 如图 c,d
    • 我们只要移动e就好(最长递增长子序列)
    vue3核心组件更新diff算法(简易)
    const patchKeyedChildren = (c1, c2, el) => {
      // ...
    
      if (i > e1) {
      } else if (i > e2) {
      } else {
          let s1 = i
          let s2 = i
    
          const keyToNewIndexMap = new Map()
    
          for (let i = s2; i <= e2; i++) {
            const childVNode = c2[i]
            keyToNewIndexMap.set(childVNode.key, i)
          }
    
          // 看这里
          // 剩下的没做比较有几个
          const toBePatched = e2 - s2 + 1
          // 将剩下的封装成一个数组, 并每个都赋值为0 
          // 以比较就将老的对应的虚拟节点 索引 赋值给当前数组的
          // 没有比较的就是0
          // 以上面的图片为例 最终结果是 [5,3,4,0]
          const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
    
          for (let i = s1; i<=e1 ; i++) {
            const oldVnode = c1[i]
            let newIndex = keyToNewIndexMap.get(oldVnode.key)
            if(newIndex === undefined) {
              // 新的没有 删除老的
              unmount(oldVnode)
            } else {
              // i+1防止是0 明明比较了 还是0
              newIndexToOldIndexMap[newIndex - s2] = i + 1
              // 在老的中找到 做了新老虚拟比较 但是位置没有移动
              patch(oldVnode, c2[newIndex], el)
            }
          }
    
    
          // 处理虚拟节点的位置 从后往前依次插入节点
          for (let i = toBePatched - 1; i >= 0; i--) {
            // 找到当前最后一个 和 当前节点
            let currentIndex = i + s2
            let child = c2[currentIndex]
    
            // 插入的参照物
            let anchor = currentIndex + 1 < c2.length ? c2[currentIndex+1].el  : null
    
            // 如果是0 说明没有被patch过 直接生成
            if(newIndexToOldIndexMap[i] == 0) {
              patch(null, child, el, anchor)
            } else {
    
              hostInsert(child.el, el, anchor)
    
            }
    
          }
    
    
      }
    
      // ...
    }
    

    步骤三

    • 把没必要移动的节点排除掉
    • 最长递增长子序列
    const patchKeyedChildren = (c1, c2, el) => {
      // ...
    
      if (i > e1) {
      } else if (i > e2) {
      } else {
          let s1 = i
          let s2 = i
    
          const keyToNewIndexMap = new Map()
    
          for (let i = s2; i <= e2; i++) {
            const childVNode = c2[i]
            keyToNewIndexMap.set(childVNode.key, i)
          }
    
          // 看这里
          // 剩下的没做比较有几个
          const toBePatched = e2 - s2 + 1
          // 将剩下的封装成一个数组, 并每个都赋值为0 
          // 以比较就将老的对应的虚拟节点 索引 赋值给当前数组的
          // 没有比较的就是0
          // 以上面的图片为例 最终结果是 [5,3,4,0]
          const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
    
          for (let i = s1; i<=e1 ; i++) {
            const oldVnode = c1[i]
            let newIndex = keyToNewIndexMap.get(oldVnode.key)
            if(newIndex === undefined) {
              // 新的没有 删除老的
              unmount(oldVnode)
            } else {
              // i+1防止是0 明明比较了 还是0
              newIndexToOldIndexMap[newIndex - s2] = i + 1
              // 在老的中找到 做了新老虚拟比较 但是位置没有移动
              patch(oldVnode, c2[newIndex], el)
            }
          }
    
          // 最长递增长子序列
          // [1,2]对应[3,4] 就是老节点数组2,3位置的节点, 不需要移动的节点
          // [5,3,4,0] => [1,2]
          let increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
          let j = increasingNewIndexSequence.length - 1
    
          // 处理虚拟节点的位置 从后往前依次插入节点
          for (let i = toBePatched - 1; i >= 0; i--) {
            // 找到当前最后一个 和 当前节点
            let currentIndex = i + s2
            let child = c2[currentIndex]
    
            // 插入的参照物
            let anchor = currentIndex + 1 < c2.length ? c2[currentIndex+1].el  : null
    
            // 如果是0 说明没有被patch过 直接生成
            if(newIndexToOldIndexMap[i] == 0) {
              patch(null, child, el, anchor)
            } else {
    
              // 从后往前查 没有就插入 
              if(i != increasingNewIndexSequence[j]){
                hostInsert(child.el, el, anchor)
              } else {
                j--;
              }
    
            }
    
          }
    
    
      }
    
      // ...
    }
    
    /**
       * @desc 最长递增子序列
       */
      function getSequence(arr) {
        const len = arr.length
        const result = [0]
        const p = arr.slice(0)
        let start;
        let end;
        let middle;
        for (let i = 0; i < len; i++) {
            const arrI = arr[i]
            if (arrI !== 0) {
                let resultLastIndex = result[result.length - 1]
                // 取到索引对应的值
                if (arr[resultLastIndex] < arrI) {
                    p[i] = resultLastIndex
                    result.push(i)
                    continue
                }
                start = 0;
                end = result.length - 1;
                while (start < end) {
                    middle = ((start + end) / 2) | 0
                    if (arr[result[middle]] < arrI) {
                        start = middle + 1
                    } else {
                        end = middle
                    }
                }
    
                if (arrI < arr[result[start]]) {
                    if (start > 0) {
                      p[i] = result[start - 1]
                    }
                    result[start] = i
                }
            }
        }
        let len1 = result.length
        let last = result[len1 - 1]
        while (len1-- > 0) {
            result[len1] = last
            last = p[last]
        }
        return result
      }
    

    最长递增长子序列

    • [2, 3, 1, 5, 6, 8, 7, 9, 4] => [0, 1, 3, 4, 6, 7]
    • vue3源码最长递增长子序列地址
    vue3核心组件更新diff算法(简易)
    * 贪心 + 二分算法如下
    * 2 
    * 2 3
    * 1 3
    * 1 3 5
    * 1 3 5 6
    * 1 3 5 6 8
    * 1 3 5 6 7 
    * 1 3 5 6 7 9
    
    * 1 3 4 6 7 9  值
    * | | | | | |
    * 2 1 8 4 6 7  索引(第一步的result)
    
    * 2 3 1 5 6 8 7 9 4  原数组值
    * | | | | | | | | |
    * X 0 X 1 3 4 4 6 1  push和二分查找的时候对应的上一个索引(p)
    
    * 2 3 5 6 7 9  希望结果值
    * | | | | | |
    * 0 1 3 4 6 7  希望结果索引(result最终返回结果)
    

    第一步

    // 第一步
    // 1 3 4 6 7 9  值
    // | | | | | |
    // 2 1 8 4 6 7  索引
    // 贪心 + 二分算法
    function getSequence(arr) {
        const len = arr.length
    
    // 存储索引
      const result = [0]
      let start;
      let end;
      let middle;
      for (let i = 0; i < len; i++) {
        const arrI = arr[i]
        if (arrI !== 0) {
            let resultLastIndex = result[result.length - 1]
            // 比前一个数值大 就将对应的索引push进去
            // 取到索引对应的值
            if (arr[resultLastIndex] < arrI) {
                result.push(i)
                continue
            }
    
            // 通过二分算法 找到比当前值大的 将当前值替代掉大的值
            start = 0
            end = result.length - 1
            while (start < end) {
                // 取中间的索引 用位运算 取整数 如下
                // (0 + 5) / 2 -> 2.5
                // 2.5 | 0 -> 2
                middle = ((start + end) / 2) | 0
                if (arr[result[middle]] < arrI) {
                    start = middle + 1
                } else {
                    end = middle
                }
            }
            
            // 最后 start == end 就是要找的对应的索引
            // 并将当前的索引替换掉
            result[start] = i
    
      }
    }
    
    return result
    }
    
    

    第二步

    // 第二步
    
    // 通过索引中最后一个索引 倒叙 去记录 每次添加的索引前一个值得缩影
    // 如上面的图所示(通过9开始追溯 9对应的索引是7)
    // 2   3   1   5   6   8   7   9   4   原先值
    //     1   2       4       6   7   8   result上一步结果是[2, 1, 8, 4, 6, 7]
    // X  0    X   1   3   4   4   6   1   每个索引 对应的上一个值得索引
    
    // 找个中间的数组 记录每一次循环的替换 push的上一个值得索引
    // 求p [2,0,1,1,3,4,4,6,1]
    
    function getSequence(arr) {
        const len = arr.length
        const result = [0]
        // copy一个同长度的数组 值无所谓
        const p = arr.slice(0)
        let start;
        let end;
        let middle;
        for (let i = 0; i < len; i++) {
            const arrI = arr[i]
            if (arrI !== 0) {
                let resultLastIndex = result[result.length - 1]
                if (arr[resultLastIndex] < arrI) {
                    // 要push值得索引 赋值给对应值得 重新赋值上上一个值的索引
                    p[i] = resultLastIndex
                    result.push(i)
                    continue
                }
                start = 0;
                end = result.length - 1;
                while (start < end) {
                    middle = ((start + end) / 2) | 0
                    if (arr[result[middle]] < arrI) {
                        start = middle + 1
                    } else {
                        end = middle
                    }
                }
    
                // 防止相同 或者大于的值
                if (arrI < arr[result[start]]) {
                    // 0的前一个值 没有 所以大于0
                    if (start > 0) {
                      p[i] = result[start - 1]
                    }
                    result[start] = i
                }
            }
        }
        
        // [2,0,1,1,3,4,4,6,1]
        console.log('求p', p)
        return result
      }
    

    第三步

    // 第三步
    // 2   3   1   5   6   8   7   9   4   原先值
    // 2   0   1   1   3   4   4   6   1   每个索引 对应的上一个值得索引
    //     1   2       4       6   7   8   result上一步结果是[2, 1, 8, 4, 6, 7]
    // 求[0, 1, 3, 4, 6, 7]
    
    function getSequence(arr) {
        const len = arr.length
        const result = [0]
        const p = arr.slice(0)
        let start;
        let end;
        let middle;
        for (let i = 0; i < len; i++) {
            const arrI = arr[i]
            if (arrI !== 0) {
                let resultLastIndex = result[result.length - 1]
                // 取到索引对应的值
                if (arr[resultLastIndex] < arrI) {
                    p[i] = resultLastIndex
                    result.push(i)
                    continue
                }
                start = 0;
                end = result.length - 1;
                while (start < end) {
                    middle = ((start + end) / 2) | 0
                    if (arr[result[middle]] < arrI) {
                        start = middle + 1
                    } else {
                        end = middle
                    }
                }
    
                if (arrI < arr[result[start]]) {
                    if (start > 0) {
                      p[i] = result[start - 1]
                    }
                    result[start] = i
                }
            }
        }
    
        // 通过p找上一值 对应的索引
        // 从末尾数索引7(原数组对应的值是9)开始找
        let len1 = result.length
        let last = result[len1 - 1]
        while (len1-- > 0) {
            result[len1] = last
            last = p[last]
        }
        return result
    }
    

    shard runtime-dom runtime-core

    • 组件的渲染流程其他模块 请看上一章vue3组件渲染流程(简易)

    renderer.ts

    import { effect } from "@vue/reactivity/src"
    import { ShapeFlags } from "@vue/shared/src"
    import { createAppAPI } from "./apiCreateApp"
    import { createComponentInstance, setupComponent } from "./component"
    import { queueJob } from "./scheduler"
    import { normalizeVNode ,Text} from "./vnode"
    
    /**
     * @description 创建一个渲染器
     * @param rendererOptions 针对浏览器的一些元素 文本的增删改查
     */
    export function createRenderer(rendererOptions) {
      /**
       * @desc 浏览器的操作方法
       */
      const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        createComment: hostCreateComment,
        setText: hostSetText,
        setElementText: hostSetElementText,
        nextSibling: hostNextSibling,
      } = rendererOptions
    
      /**
       * @desc 创建组件的effect函数 执行render函数
       */
      const setupRenderEffect = (instance, container) => {
        instance.update = effect(function componentEffect() {
          if (!instance.isMounted) {
            let proxyToUse = instance.proxy
            let subTree = instance.subTree = instance.render.call(proxyToUse, proxyToUse)
    
            // 用render函数的返回值 继续渲染
            patch(null, subTree, container)
            instance.isMounted = true
          } else {
            // 更新 diff vnode
            const prevTree = instance.subTree
            let proxyToUse = instance.proxy
            const nextTree = instance.render.call(proxyToUse, proxyToUse)
    
            patch(prevTree, nextTree, container)
          }
        }, {
          scheduler: queueJob
        })
    
      }
    
      /**
       * @desc 挂载组件
       * @param initialVNode 初始化vnode
       * @param container    dom
       */
      const mountComponent = (initialVNode, container) => {
        // 1.先有实例
        const instance = (initialVNode.component = createComponentInstance(initialVNode))
    
        // 2.需要的数据解析到实例上 如 state props attrs render...
        setupComponent(instance)
    
        // 3.创建effect函数 执行render函数
        setupRenderEffect(instance, container)
      }
    
      /**
       * @desc 加工组件
       */
      const processComponent = (n1, n2, container) => {
        if (n1 == null) {
          // 初次渲染
          mountComponent(n2, container)
        } else {
          // update...
        }
      }
    
      /**
       * @desc 挂载孩子
       */
      const mountChildren = (children, container) => {
        for (let i = 0; i < children.length; i++) {
          // 孩子有可能存在的文本数组 如['1', 'a']
          // 创建文本的虚拟节点
          // patch时 创建文本节点 将文本节点插入到元素中
          let child = normalizeVNode(children[i])
          patch(null, child, container)
        }
      }
    
      /**
       * @desc 挂载元素
       */
      const mountElement = (vnode, container, anchor = null) => {
        const { props, shapeFlag, type, children } = vnode
        let el = (vnode.el = hostCreateElement(type))
    
        // 属性生成
        if (props) {
          for (const key in props) {
              hostPatchProp(el, key, null, props[key]);
          }
        }
    
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(el, children)
        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(children, el)
        }
    
        hostInsert(el, container, anchor)
      }
    
      /**
       * @desc 加工元素
       */
      const processElement = (n1, n2, container, anchor) => {
        if (n1 == null) {
          mountElement(n2, container, anchor)
        } else {
          // diff Element
          patchElement(n1, n2, container)
    
        }
      }
    
      /**
       * @desc 加工文本
       */
      const processText = (n1, n2, container) => {
        if (n1 == null) {
          hostInsert((n2.el = hostCreateText(n2.children)), container)
        }
      }
    
      /** ******** diff module ******** */
      /**
       * @param n1          oldVNode
       * @param n2          newVNode
       * @param container   Dom
       * @param anchor      参照物节点
       */
      /**
       * @desc 比较两个虚拟节点 是否一致
       */
      const isSameVNodeType = (n1, n2) => {
        return n1.type === n2.type && n1.key === n2.key
      }
    
      /**
       * @desc 销毁元素, 组件
       */
      const unmount = (n1) => {
        // 源码中如果是组件(这里就展示)
        // 调用的组件的生命周期 调用组件的销毁方法等
        hostRemove(n1.el)
      }
    
      /**
       * @desc 销毁孩子元素, 孩子组件
       */
      const unmountChildren = (children) => {
        for (let i = 0; i < children.length; i++) {
          unmount(children[i])
        }
      }
    
      /**
       * @desc 比对属性
       */
      const patchProps = (oldProps, newProps, el) => {
        if (oldProps !== newProps) {
          for (let key in newProps) {
            const prev = oldProps[key]
            const next = newProps[key]
            if (prev !== next) {
              hostPatchProp(el, key, prev, next)
            }
          }
          for (const key in oldProps) {
            if (!(key in newProps)) {
              hostPatchProp(el, key, oldProps[key], null)
            }
          }
        }
      }
    
      /**
       * @desc 核心diff 比对孩子
       * @param c1 老的孩子
       * @param c2 新的孩子
       * @param el 父元素
       */
      const patchKeyedChildren = (c1, c2, el) => {
        // Vue3都是默认从头开始比对
        let i = 0
        let e1 = c1.length - 1
        let e2 = c2.length - 1
    
        // diff children 1 从头开始比 不同的就停止
        while (i <= e1 && i <= e2) {
          const n1 = c1[i]
          const n2 = c2[i]
          if (isSameVNodeType(n1, n2)) {
            patch(n1, n2, el)
          } else {
            break;
          }
    
          i++;
        }
    
        // diff children 2 从尾开始比 不同的就停止
        while (i <= e1 && i <= e2) {
          const n1 = c1[e1]
          const n2 = c2[e2]
          if (isSameVNodeType(n1, n2)) {
              patch(n1, n2, el)
          } else {
              break;
          }
          e1--;
          e2--;
        }
    
        if (i > e1) {
          // diff children 3 老的少 新的多(有一方比对完成)
          // 两种情况从前往后比时 和 从后往前比时(同一个逻辑)
          if (i <= e2) {
            // 想知道是向前插入 还是向后插入
            const nextPos = e2 + 1
            const anchor = nextPos < c2.length ? c2[nextPos].el : null
            while (i <= e2) {
              patch(null, c2[i], el, anchor)
              i++;
            }
          }
        } else if (i > e2) {
          // diff children 4 老的多 新的少(有一方比对完成)
          while (i <= e1) {
            unmount(c1[i])
            i++;
          }
        } else {
          // diff children 5 乱序比较
          // 用新的元素做成一个映射表去老的里找
          // 一样的就复用 不一样的要不插入 要不删除
          let s1 = i
          let s2 = i
    
          // 用的是新的做的映射表
          const keyToNewIndexMap = new Map()
    
          // 结果如: { e=>2, c=>3, d=>4, h=>5 }
          for (let i = s2; i <= e2; i++) {
            const childVNode = c2[i]
            keyToNewIndexMap.set(childVNode.key, i)
          }
    
          // 剩下的没做比较有几个
          const toBePatched = e2 - s2 + 1
          // 将剩下的封装成一个数组, 并每个都赋值为0 
          // 以比较就将老的对应的虚拟节点 索引 赋值给当前数组的
          // 没有比较的就是0
          const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
    
          for (let i = s1; i<=e1 ; i++) {
            const oldVnode = c1[i]
            let newIndex = keyToNewIndexMap.get(oldVnode.key)
            if(newIndex === undefined) {
              // 新的没有 删除老的
              unmount(oldVnode)
            } else {
              // i+1防止是0 明明比较了 还是0
              newIndexToOldIndexMap[newIndex - s2] = i + 1
              // 在老的中找到 做了新老虚拟比较 但是位置没有移动
              patch(oldVnode, c2[newIndex], el)
            }
          }
    
          // 最长递增长子序列
          // [1,2]对应[3,4] 就是老节点数组2,3位置的节点, 不需要移动的节点
          // [5,3,4,0] => [1,2]
          let increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
          let j = increasingNewIndexSequence.length - 1
    
          // 处理虚拟节点的位置 从后往前依次插入节点
          for (let i = toBePatched - 1; i >= 0; i--) {
            // 找到当前最后一个 和 当前节点
            let currentIndex = i + s2
            let child = c2[currentIndex]
    
            // 插入的参照物
            let anchor = currentIndex + 1 < c2.length ? c2[currentIndex+1].el  : null
    
            // 如果是0 说明没有被patch过
            if(newIndexToOldIndexMap[i] == 0) {
              patch(null, child, el, anchor)
            } else {
              
              // 从后往前查 没有就插入 
              if(i != increasingNewIndexSequence[j]){
                hostInsert(child.el, el, anchor)
              } else {
                j--;
              }
    
            }
    
          }
          
        }
    
      }
    
      /**
       * @desc 比对孩子
       */
      const patchChildren = (n1, n2, el) => {
        const c1 = n1.children
        const c2 = n2.children
    
        const prevShapeFlag = n1.shapeFlag
        const shapeFlag = n2.shapeFlag
    
        // c2如果是文本
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          // 3.老的是数组 新的是文本 销毁老的
          if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            unmountChildren(c1)
          }
    
          // 4.都是文本
          if (c1 !== c2) {
            hostSetElementText(el, c2)
          }
    
        } else {
          // c2否则是null 或者 数组
          if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
              // 都是数组
              // 核心diff
              patchKeyedChildren(c1, c2, el)
            } else {
              // 5.null 特殊情况 删除掉老的
              unmountChildren(c1)
            }
    
          } else {
            // 6.老的是文本 现在是数组
            if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
              hostSetElementText(el, '');
            }
    
            if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
              mountChildren(c2, el);
            }
    
          }
    
        }
    
        
      }
    
      /**
       * @desc 比对元素
       */
      const patchElement = (n1, n2, container) => {
        // 相同节点
        let el = (n2.el = n1.el)
    
        // 2.比对属性
        const oldProps = n1.props || {}
        const newProps = n2.props || {}
        patchProps(oldProps, newProps, el)
    
        // 比对孩子
        patchChildren(n1, n2, el)
      }
    
      /**
       * @desc 比对
       * @param n1          老的vnode
       * @param n2          新的vnode
       * @param container   dom
       * @param anchor      参照物节点
       */
      const patch = (n1, n2, container, anchor = null) => {
        const { shapeFlag, type } = n2
    
        // 1.type 不同直接替换
        if (n1 && !isSameVNodeType(n1, n2)) {
          anchor = hostNextSibling(n1.el)
          unmount(n1)
          n1 = null
        }
    
        switch (type) {
          case Text:
            processText(n1, n2, container)
            break;
        
          default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
              processElement(n1, n2, container, anchor)
            } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
              processComponent(n1, n2, container)
            }
    
            break;
        }
      }
    
      /**
       * @desc 渲染
       * @param vnode     vnode
       * @param container dom
       */  
      const render = (vnode, container) => {
        // 核心 根据vnode -> dom
        patch(null, vnode, container)
      }
    
      /**
       * @desc 最长递增子序列
       */
      function getSequence(arr) {
        const len = arr.length
        const result = [0]
        const p = arr.slice(0)
        let start;
        let end;
        let middle;
        for (let i = 0; i < len; i++) {
            const arrI = arr[i]
            if (arrI !== 0) {
                let resultLastIndex = result[result.length - 1]
                // 取到索引对应的值
                if (arr[resultLastIndex] < arrI) {
                    p[i] = resultLastIndex
                    result.push(i)
                    continue
                }
                start = 0;
                end = result.length - 1;
                while (start < end) {
                    middle = ((start + end) / 2) | 0
                    if (arr[result[middle]] < arrI) {
                        start = middle + 1
                    } else {
                        end = middle
                    }
                }
    
                if (arrI < arr[result[start]]) {
                    if (start > 0) {
                      p[i] = result[start - 1]
                    }
                    result[start] = i
                }
            }
        }
        let len1 = result.length
        let last = result[len1 - 1]
        while (len1-- > 0) {
            result[len1] = last
            last = p[last]
        }
        return result
      }
    
      return {
        createApp: createAppAPI(render)
      }
    }
    
    


    起源地下载网 » vue3核心组件更新diff算法(简易)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元