Vue - diff原理

92 阅读5分钟

四个方法

sameVnode

sameVnode方法用于比较两个节点是否是同类型的节点(key、tag等属性相同),决定是否复用节点

function sameVnode(a, b) {
  // Tips: 判断条件删除了部分判断条件
  return (
    a.key === b.key && // key是否相同
    a.tag === b.tag && // tag是否相同
    a.isComment === b.isComment && // 是否注释节点
    isDef(a.data) === isDef(b.data) && // data是否都已定义或者未定义
    sameInputType(a, b) // 节点为input时,判断input.type是否相同
  )
}

patch

patch方法diff流程的入口。会简单的判断下有无新节点旧节点

  • 没有新节点:结束patch方法的执行
  • 没有旧节点,但有新节点:不需比较,直接创建对应的新元素
  • 有旧节点,且有新节点:调用sameVnode方法判断节点是否可复用
    • 可复用节点:调用patchVnode方法继续比较新、旧节点
    • 不可复用节点:直接创建对应的新元素,并移除旧节点
function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    // Cond: 没有新节点,结束patch方法的执行
    if (isDef(oldVnode)) {
      invokeDestroyHook(oldVnode)
    }
    return
  }

  const insertedVnodeQueue: any[] = []

  if (isUndef(oldVnode)) {
    // Cond: 没有旧节点,只有新节点
    // 直接创建对应元素
    createElm(vnode, insertedVnodeQueue)
  } else {
    // Cond: 有旧节点,有新节点
    // 判断节点是否可复用
    if (sameVnode(oldVnode, vnode)) {
      // Cond: 可复用节点
      // 执行patchVnode比较新旧节点
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // Cond: 不可复用节点
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // 创建新元素
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // 移除旧节点
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  return vnode.elm
}

patchVnode

patchVnode方法用于比较新节点旧节点。会比较新节点旧节点

  • 新节点非文本节点:判断新节点、旧节点是否有子节点
    • 有新子节点,且有旧子节点:调用updateChildren方法比较新、旧子节点
    • 有新子节点,但没有旧子节点:直接添加新子节点
    • 没有新子节点,但有旧子节点:直接移除旧子节点
    • 没有新子节点,且没有旧子节点,且旧节点是文本节点:清空文本内容
  • 新节点是文本节点,且新节点的文本内容与旧节点不同:更新文本内容
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly?: any
) {
  if (oldVnode === vnode) {
    // Cond: 旧节点与新节点相等
    // 直接结束patchVnode的执行
    return
  }


  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // 克隆复用节点
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = (vnode.elm = oldVnode.elm)

  // diff优化:对于克隆的静态节点直接复用其元素,并结束patchVnode的执行
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  const oldCh = oldVnode.children
  const ch = vnode.childrenMatch

  if (isUndef(vnode.text)) {
    // Cond: 非文本节点
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) {
        // Cond: 有旧子节点数组,且有新子节点数组,且二者不相等
        // 调用updateChildren方法比较子节点数组
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      }
    } else if (isDef(ch)) {
      // Cond: 有新子节点数组,且没有旧子节点数组
      if (isDef(oldVnode.text)) {
        // Cond: 旧节点是文本节点
        // 清空文本内容
        nodeOps.setTextContent(elm, '')
      }

      // 添加新子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // Cond: 有旧子节点数组,且没有新子节点数组
      // 移除旧子节点
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // Cond: 旧节点是文本节点
      // 清空文本内容
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // Cond: 文本节点且文本内容不同
    // 更新文本内容
    nodeOps.setTextContent(elm, vnode.text)
  }
}

updateChildren

updateChildren方法用于比较新、旧子节点,会从两端向中间对比新、旧子节点数组,因此需要先了解几个自定义的子节点名称:

  • 旧前节点:旧子节点数组左侧的节点
  • 旧后节点:旧子节点数组右侧的节点
  • 新前节点:新子节点数组左侧的节点
  • 新后节点:新子节点数组右侧的节点
第一步

依次判断如下条件,满足条件时执行对应条件分支语句,然后开始下一次循环:

  • 没有旧前节点:获取下一个旧前节点
  • 没有旧后节点:获取下一个旧后节点
  • 新前节点可复用旧前节点:调用patchVnode方法,获取下一个新前、旧前节点
  • 新后节点可复用旧后节点:调用patchVnode方法,获取下一个新后、旧后节点
  • 新后节点可复用旧前节点:调用patchVnode方法,将旧前节点对应的元素移到旧后节点前,获取下一个新后、旧前节点
  • 新前节点可复用旧后节点:调用patchVnode方法,将旧后节点对应的元素移到旧前节点前,获取下一个新前、旧后节点
  • 以上条件都不满足:根据新前节点的key判断旧子节点中是否有与之对应的子节点,然后获取下一个新前节点
    • 没有对应的子节点:直接创建新的元素
    • 有对应的子节点:调用sameVnode,判断节点是否可复用
      • 可复用:调用patchVnode,将复用节点的元素移到旧前节点前
      • 不可复用:直接创建新的元素
第二步

循环结束后,若旧子节点数组中有未处理的节点,则移除;若新子节点数组中有未处理的节点,则新增

function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  let oldStartIdx = 0 // 旧前子节点的索引
  let newStartIdx = 0 // 新前子节点的索引
  let oldEndIdx = oldCh.length - 1 // 旧后子节点的索引
  let oldStartVnode = oldCh[0] // 旧前子节点
  let oldEndVnode = oldCh[oldEndIdx] // 旧后子节点
  let newEndIdx = newCh.length - 1 // 新后子节点的索引
  let newStartVnode = newCh[0] // 新前子节点
  let newEndVnode = newCh[newEndIdx] // 新后子节点
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // 开始循环遍历新、旧子节点数组
  // 需要清晰的是:
  // 当新、旧子节点数组其一遍历完成,则结束while循环
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // Cond: 无旧前子节点
      // 获取下一个旧前子节点
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (isUndef(oldEndVnode)) {
      // Cond: 无旧后子节点
      // 获取下一个旧后子节点
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // Cond: 新前子节点可复用旧前节点
      // 调用patchVnode比较新子节点与旧子节点
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )

      // 获取下一个旧前、新前子节点
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // Cond: 新后子节点可复用旧后子节点
      // 调用patchVnode比较新子节点与旧子节点
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )

      // 获取下一个旧前、新后子节点
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Cond: 新后子节点可复用旧前子节点
      // 调用patchVnode比较新子节点与旧子节点
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      )

      // 将旧前节点对应的元素移动到右侧(旧后节点对应的元素之前)
      nodeOps.insertBefore(
        parentElm,
        oldStartVnode.elm,
        nodeOps.nextSibling(oldEndVnode.elm)
      )

      // 获取下一个旧前、新后子节点
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Cond: 新前子节点可复用旧后子节点
      // 调用patchVnode比较新子节点与旧子节点
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      )

      // 将旧后节点对应的元素移动到左侧(旧前节点对应的元素之前)
      nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)

      // 获取下一个旧后、新前子节点
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // Cond: 以上条件都不满足
      // 获取未遍历的旧子节点数组中的节点的key与索引对应的关系对象:{ [key]: [index] }
      if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      // 首先会判断旧子节点数组中是否有与新前节点key相同的节点
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      
      if (isUndef(idxInOld)) {
        // Cond: 没有在旧子节点数组找到与新前节点对应的节点
        // 直接创建新元素
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        )
      } else {
        // Cond: 有新前节点对应的旧节点
        // 判断节点是否可复用
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // Cond: 可复用
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          )
          oldCh[idxInOld] = undefined

          // 将复用的节点移动到左侧(旧前节点之前)
          nodeOps.insertBefore(
            parentElm,
            vnodeToMove.elm,
            oldStartVnode.elm
          )
        } else {
          // Cond: 不可复用
          // 直接创建新元素
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        }
      }

      // 获取下一个新前节点
      newStartVnode = newCh[++newStartIdx]
    }
  }

  // 结束循环后
  // 若是旧子节点遍历完,则新增未遍历到的新子节点
  // 若是新子节点遍历完,则移除未遍历到的旧子节点
  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)
  }
}