虚拟Dom和diff算法

80 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天

虚拟DOM(VDOM)
定义是一个 「js对象」, 是真实DOM的抽象, 比DOM轻量, 参考的是snabbdom
好处1.避免重复操作DOM, 避免重绘和回流,这样性能比较高;     2. 跨平台

vue2源码

/*
    * oldVnode: 旧虚拟节点
    * vnode:   新虚拟节点
    * hydrating: 是否和真实dom混合
    * removeOnly: 标识
    *
*/
// path()入口
function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 情况1: vnode不存在, oldVnode存在, 移除oldVnode
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []
    
    // 情况2: oldVnode不存在,创建新节点
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 情况3:  vnode存在,oldVnode存在
             // 情况3-1: 判断oldVnode是否是虚拟odm
      const isRealElement = isDef(oldVnode.nodeType)
             // 情况3-2: 判断oldVnode 和 vnode 是不是同一节点? patchVode() : 挂载真实dom
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 真实dom
        if (isRealElement) {
          ...
        }
         ...
      }
    }
  }

// patchVnode()
 function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 情况一: vnode === oldVnode 直接返回
    if (oldVnode === vnode) {
      return
    }
    
    ...

    const oldCh = oldVnode.children
    const ch = vnode.children
    ...
    
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 情况二: oldCh !== ch
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
       // 情况三: oldVnode children不存在 添加节点
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 情况四: vnode children不存在 删除oldCh节点
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
   } else if (oldVnode.text !== vnode.text) {
      // 情况5: 都是文本,值不相等 - 修改
      nodeOps.setTextContent(elm, vnode.text)
   }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
  
  // updateChildren()
   function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // old开始位置
    let newStartIdx = 0 // new开始位置
    let oldStartVnode = oldCh[0]  // old 开始节点
    let newStartVnode = newCh[0] // new 开始节点
    
    let oldEndIdx = oldCh.length - 1 // old结束位置
    let newEndIdx = newCh.length - 1 // new 结束位置
    let oldEndVnode = oldCh[oldEndIdx] // old结束节点
    let newEndVnode = newCh[newEndIdx] // new结束节点
    
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 情况一: 如果节点 头 相等, 从左往右开始patchVnode
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // patch完成,移动位置
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 情况二: 如果节点 尾 相等, 从右往左开始patchVnode
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // patch完成,移动位置
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        // 情况三: 老开始 == 新结束节点, 从右往左patchvnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
       
        // 把oldStart节点放到oldEnd节点后面
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // patch 移动位置
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // 情况四: 老结束 == 新开始节点, 从右=左往右patchvnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // 把oldEnd节点放到oldStart节点前面
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // patch 移动位置
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
          // 没有就创建元素
          ...
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

vue3源码

// 情况1:  没有key
const patchUnkeyedChildren = (
    c1: VNode[], // 老节点
    c2: VNodeArrayChildren, // 新节点
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length // 老节点长度
    const newLength = c2.length // 新节点长度
    const commonLength = Math.min(oldLength, newLength) // 小长度的作为公共长度
    let i
    // 重点是「 循环 」
    for (i = 0; i < commonLength; i++) {
        const nextChild = (c2[i] = optimized ?
            cloneIfMounted(c2[i] as VNode) :
            normalizeVNode(c2[i]))
        patch(
            c1[i],
            nextChild,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
        )
    }
    // 老节点长度 > 新节点的长度 ? 删除多余节点 : 新增节点
    if (oldLength > newLength) {
        unmountChildren(
            c1,
            parentComponent,
            parentSuspense,
            true,
            false,
            commonLength
        )
    } else {
        mountChildren(
            c2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            commonLength
        )
    }
}


// 情况2: 有key--这是主要的diff算法

const patchKeyedChildren = (
    c1: VNode[], // 老节点
    c2: VNodeArrayChildren, // 新节点
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
) => {
    let i = 0 // 用于记录索引
    const l2 = c2.length // 新节点长度
    let e1 = c1.length - 1 // e1: 老节点结束位置
    let e2 = l2 - 1 // e2: 新节点结束位置
    
    // 从开始位置开始比较-循环
    while (i <= e1 && i <= e2) {
        const n1 = c1[i] // 老节点 第i个元素
        const n2 = (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i]))
            // 节点相同时
        if (isSameVNodeType(n1, n2)) {
            patch(
                n1,
                n2,
                container,
                null,
                parentComponent,
                parentSuspense,
                isSVG,
                slotScopeIds,
                optimized
            )
        } else {
            // 节点不相同就 break
            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(
                n1,
                n2,
                container,
                null,
                parentComponent,
                parentSuspense,
                isSVG,
                slotScopeIds,
                optimized
            )
        } else {
            // 节点不同时break
            break
        }
        e1--
        e2--
    }

    // 如果旧节点多 - 删除老节点, 如果新节点多 - 增加节点
    if (i > e1) {
        if (i <= e2) {
            const nextPos = e2 + 1
            const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
            while (i <= e2) {
                patch(
                    null,
                    (c2[i] = optimized
                        ? cloneIfMounted(c2[i] as VNode)
                        : normalizeVNode(c2[i])),
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized
                )
                i++
            }                                                                                                                                                                                                                 
        }
    }

    else if (i > e2) {
        while (i <= e1) {
            unmount(c1[i], parentComponent, parentSuspense, true)
            i++
        }
    }

    // 特殊情况处理:首尾相同中间乱序
    // 接着1-2 的逻辑往下走
    else {
        const s1 = i // prev starting index
        const s2 = i // next starting index

        // 5.1 把没有匹配到的新节点通过map保存「 newIndexToOldIndexMap」
        const keyToNewIndexMap: Map<string | number, number> = new Map()
        
        for (i = s2; i <= e2; i++) {
            const nextChild = (c2[i] = optimized
                ? cloneIfMounted(c2[i] as VNode)
                : normalizeVNode(c2[i]))

            // 遍历新的节点,为新节点设置key
            if (nextChild.key != null) {
                if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
                    ...
                }
                keyToNewIndexMap.set(nextChild.key, i)
            }
        }

      
        let j
        let patched = 0  // 记录-已经patch的新节点的数量
        const toBePatched = e2 - s2 + 1 // 记录-没有经过 path 新的节点的数量
        let moved = false
        
        let maxNewIndexSoFar = 0
        
        const newIndexToOldIndexMap = new Array(toBePatched)
        
        // 遍历老节点
        for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

        for (i = s1; i <= e1; i++) {
            const prevChild = c1[i]
            // 统一卸载老节点
            if (patched >= toBePatched) {
                unmount(prevChild, parentComponent, parentSuspense, true)
                continue
            }
            let newIndex
            if (prevChild.key != null) {
                // 老节点的key存在 ,通过key找到对应的index
                newIndex = keyToNewIndexMap.get(prevChild.key)
            } else {
                // 老节点的key不存在, 遍历剩下的所有新节点
                for (j = s2; j <= e2; j++) {
                    if (
                        newIndexToOldIndexMap[j - s2] === 0 &&
                        isSameVNodeType(prevChild, c2[j] as VNode)
                    ) {
                        newIndex = j
                        break
                    }
                }
            }
            if (newIndex === undefined) {
                unmount(prevChild, parentComponent, parentSuspense, true)
            } else {
                newIndexToOldIndexMap[newIndex - s2] = i + 1
                if (newIndex >= maxNewIndexSoFar) {
                    maxNewIndexSoFar = newIndex
                } else {
                    moved = true
                }
                patch(
                    prevChild,
                    c2[newIndex] as VNode,
                    container,
                    null,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized
                )
                patched++
            }
        }

        // 5.3 move and mount
       
        const increasingNewIndexSequence = moved
            ? getSequence(newIndexToOldIndexMap)
            : EMPTY_ARR
        j = increasingNewIndexSequence.length - 1
        
        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) {
                patch(
                    null,
                    nextChild,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized
                )
            } else if (moved) {
                
                if (j < 0 || i !== increasingNewIndexSequence[j]) {
                    move(nextChild, container, anchor, MoveType.REORDER)
                } else {
                    j--
                }
            }
        }
    }
}



vue2 和vue3 diff区别总结

Diff算法Vue2Vue3
定义通过「同层的树节点」进行比较的「高效算法」
特点1. 仅「同层级进行」     2. 比较过程中, 由两边向中间比较
原理递归 + 双指针最长递增子序列
源码分析 1. 判断是不是同一元素,不是,则「直接替换」
2. patch(oldVnode, Vnode)- diff算法入口函数同一元素,比较{「 属性和children 」    
   a. oldVnode是否是虚拟dom    
   b. oldVnode和Vnode是否是同一节点,否-重建
3. children五种情况:- patchVnode(精细化比较)  
   a. child 新 == 老, return
   b. child 新有,老有,都是text, 不相等,修改即可   
   c. child新无,老有,删除老的  
   d. child新有,老无,「在老的上面创建子节点」
   e. child新有,老有(「 双指针」updateChildren)
4. 双指针比对 updateChildren()
   a. 条件1:  头头比较,
   b. 条件2:  尾尾比较,
   . 条件3:  头尾比较,
   d. 条件4:  尾头比较
   e. 条件5,abcd条件不满足,分两种情况
      i. 无key: newStartVnode生成新的节点插入到真实DOM中
     ii. 有key:未处理的节点的key组成的hash表对比
没有key:

1. 获取新老节点长度最小的,循环进行patch工作
2. 新节点长度 > 旧节点长度, 挂载公共部分之后的所有新节点
3. 新节点长度 < 旧节点长度, 卸载公共部分之后的所有老节点

 

有key:

1. 头头比较,相同的节点patch ,一发现不同,立即跳出。
2. 若第一步没patch完,则尾尾比较,相同就patch ,一发现不同,立即跳出循环。
3. 若new的节点大于old的节点数 ,剩下的节点全部以新的vnode处理
4. 若老的节点大于新的节点数,超出的节点全部卸载 。
5. 与 3 ,4对立(基于最长递增子序列进行移动/添加/删除)
     a. 把没有比较过的新的vnode节点,通过map保存
     b . 遍历所有新节点把索引和对应的key, 存入map keyToNewIndexMap中
     c. 遍历老节点,判断有无key, 有,则通过keyToNewIndexMap找到新节点的 key和index, 无,则遍历剩下的新节点,试图找到对应的index
     d. 存在index, 证明有相同节点,复用老节点patch
     e. 不存在index, 删除当前老节点
  g. 遍历过程可能会发生移动: 根据 newIndexToOldIndexMap 新老节点索引列表找到最长稳定序列