vue2源码-十、diff算法

diff算法

diff算法的特点就是平级比较,内部采用了双指针方式进行优化,优化了常见的操作。采用了递归比较的方式。

  • 针对一个节点的diff算法

    • 先拿出根节点来进行比较如果是同一个节点则比较属性,如果不是同一个节点则直接换成最新的即可。
    • 同一个节点比较属性后,复用老节点
  • 比较儿子

    • 一方有儿子 一方没有儿子(删除、添加)
    • 两方都有儿子
      • 优化比较 头头 尾尾 交叉比较
      • 就做一个映射表,用新的去映射表中查找此元素是否存在,存在则移动不存在则插入,最后删除多余的
      • 这里会有多移动的情况(vue3 最长递增子序列)
  • 源码分析

    当数据发生变化时,set方法会调用Dep.notify通知所有的WatcherWatcher就会调用patch给真实DOm打补丁,更新相应的视图。

    function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
      // 如果新旧节点相同,则直接返回,不需要做任何操作
      if (oldVnode === vnode) {
        return
      }
    
      // 判断vnode是否合法,如果合法,则执行createElm方法创建真实DOM元素。
      if (isUndef(vnode.componentInstance) && isDef(vnode.tag)) {
        if (true) {
          if (vnode.ns) {
            // ...
          } else if (config.ignoredElements.length &&
                     config.ignoredElements.some(ignore => {
                       return isRegExp(ignore)
                         ? ignore.test(vnode.tag)
                         : ignore === vnode.tag
                     })) {
            // ...
          }
        }
        createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        return
      }
    
      // 获取新旧节点的DOM元素
      const oldElm = oldVnode.elm
      const elm = vnode.elm
    
      // 获取旧节点对应的上下文信息
      const oldCtx = oldVnode.context
      
      // 递归处理组件类型的节点,更新其子树
      if (isDef(vnode.data)) {
        if (isDef(i = vnode.data.hook) && isDef(i = i.prepatch)) {
          i(oldVnode, vnode)
        }
        if (isDef(i = vnode.data.hook) && isDef(i = i.init)) {
          i(vnode, true /* hydrating */)
        }
      }
    
      // 判断vnode是否为文本类型节点
      if (isTrue(vnode.text)) {
        if (isDef(oldVnode.text)) {
          if (oldVnode.text !== vnode.text) {
            // 更新文本类型节点的内容
            nodeOps.setTextContent(elm, vnode.text)
          }
        } else if (isDef(oldVnode.children)) {
          // 如果旧节点有子节点,则删除其所有子节点,并设置text内容
          removeVnodes(oldVnode, oldVnode, 0, oldVnode.children.length - 1)
        }
        return
      }
    
      // 处理非文本类型节点
      if (isDef(vnode.children) && isDef(oldVnode.children)) {
        if (vnode.children !== oldVnode.children) {
          // 对比新旧节点的子节点,更新DOM树
          updateChildren(parentElm, oldVnode.children, vnode.children, insertedVnodeQueue, removeOnly)
        }
      } else if (isDef(vnode.children)) {
        // 如果只有新节点有子节点,则将新节点的子节点加入到DOM中
        if (true) {
          checkDuplicateKeys(vnode)
        }
        if (isDef(oldVnode.text)) {
          nodeOps.setTextContent(elm, '')
        }
        addVnodes(elm, null, vnode.children, 0, vnode.children.length - 1, insertedVnodeQueue)
      } else if (isDef(oldVnode.children)) {
        // 如果只有旧节点有子节点,则将其所有子节点从DOM中移除
        removeVnodes(oldVnode, oldVnode, 0, oldVnode.children.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 如果旧节点是文本类型,则将其内容清空
        nodeOps.setTextContent(elm, '')
      }
    
      // 执行更新完毕的钩子函数
      if (isDef(i = vnode.data) && isDef(i = i.hook) && isDef(i = i.postpatch)) {
        i(oldVnode, vnode)
      }
    }
    

    patch函数前两个参数为为oldVnodeVnode,分别代表新的节点和之前的旧节点,主要做了四个判断:

    • 没有新节点,直接触发旧节点的destory钩子
    • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用createElm
    • 旧节点和新节点一样,通过sameVnode判断节点是否一样,一样时,直接调用patchVnode去处理这两个节点。
    • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点。

    下面介绍patchVnode方法

    patchVnode主要做一下判断:

    • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容。
    • 新节点和旧节点都有子节点,则处理比较更新子节点。
    • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新的DOM,并且添加进父节点。
    • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM删除

    子节点不完全一致,则调用updateChildren

    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0 // 旧头索引
        let newStartIdx = 0 // 新头索引
        let oldEndIdx = oldCh.length - 1 // 旧尾索引
        let newEndIdx = newCh.length - 1 // 新尾索引
        let oldStartVnode = oldCh[0] // oldVnode的第一个child
        let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
        let newStartVnode = newCh[0] // newVnode的第一个child
        let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
        // removeOnly is a special flag used only by <transition-group>
        // to ensure removed elements stay in correct relative positions
        // during leaving transitions
        const canMove = !removeOnly
    
        // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          // 如果oldVnode的第一个child不存在
          if (isUndef(oldStartVnode)) {
            // oldStart索引右移
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    
          // 如果oldVnode的最后一个child不存在
          } else if (isUndef(oldEndVnode)) {
            // oldEnd索引左移
            oldEndVnode = oldCh[--oldEndIdx]
    
          // oldStartVnode和newStartVnode是同一个节点
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // patch oldStartVnode和newStartVnode, 索引左移,继续循环
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
    
          // oldEndVnode和newEndVnode是同一个节点
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // patch oldEndVnode和newEndVnode,索引右移,继续循环
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
    
          // oldStartVnode和newEndVnode是同一个节点
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            // patch oldStartVnode和newEndVnode
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            // oldStart索引右移,newEnd索引左移
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
    
          // 如果oldEndVnode和newStartVnode是同一个节点
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            // patch oldEndVnode和newStartVnode
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // oldEnd索引左移,newStart索引右移
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
    
          // 如果都不匹配
          } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    
            // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    
            // 如果未找到,说明newStartVnode是一个新的节点
            if (isUndef(idxInOld)) { // New element
              // 创建一个新Vnode
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
    
            // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
            } else {
              vnodeToMove = oldCh[idxInOld]
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
                warn(
                  'It seems there are duplicate keys that is causing an update error. ' +
                  'Make sure each v-for item has a unique key.'
                )
              }
    
              // 比较两个具有相同的key的新节点是否是同一个节点
              //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // patch vnodeToMove和newStartVnode
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
                // 清除
                oldCh[idxInOld] = undefined
                // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
                // 移动到oldStartVnode.elm之前
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    
              // 如果key相同,但是节点不相同,则创建一个新的节点
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
              }
            }
    
            // 右移
            newStartVnode = newCh[++newStartIdx]
          }
        }
    

    while循环主要处理了以下五种情景:

    • 当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1
    • 当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1
    • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1
    • 当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1
    • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
      • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面
      • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置
posted @ 2023-04-20 19:30  楸枰~  阅读(33)  评论(0编辑  收藏  举报