Vue 源码中 的 diff 以及 对 key 的使用,以及和 react 之间的区别

一直不想写 vue diff 的过程,因为 这一块网络上讲的人实在是太多了,这里写了也只不过是拾人牙慧罢了,但是不写吧,又觉得心痒痒的,毕竟前面写了一篇 react 源码中 对 key 的使用 的

首先是几个工具函数

// 这里用来判断 一个值是否是 未定义的
export function isDef(v: any): boolean % checks {
  return v !== undefined && v !== null
}
// 对比 是不是 新节点
// 这里和之前的区别是, 没有设置 key 的情况下,key 被认为是相等的
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    if (isDef(c) && sameVnode(node, c)) return i
  }
}
// 两个 vnode 是否相同
function sameVnode(a, b) {
  return (
    // 需要注意的是 null === null 为 true
    // 也就是说,如果 没有设置 key 的话,那么就默认为 两个节点的key 是一致的
    // 对比于 react 的diff ,这里的判断 多出了 tag、comment 和 input 的判断
    // 而 react 则着重于 $$type 的判断
    // 这里 react 明显的 对于 jsx 的分类 更倾向于 自己的判断,而 vue 则是 倾向于 用户的输入
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        // 是否是 同一个类型的 input 标签,这个很好理解
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
// 是否是 相同类型的 input 节点,比如 radio,text,checkbox等
function sameInputType(a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

真正 diff 的过程

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

    // 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

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }
    // 而这里,也是和 react diff 的最大的不同之处了,
    // react 对于新旧节点只做了顺序的 查找 相同节点,而 vue 还做了 前后节点的 对比
    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(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // 新旧节点的 排尾 对比,相等,则复用
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        // 旧节点的 排头 和新节点 的 排尾,相等,则复用
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        // 旧节点的排尾 和新节点的 排头,相等,则复用
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 这里是 为 老节点的 key 和 位置 做一个 对应关系
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 查找 在 老节点的 key 对应 map 中有没有 可以复用的节点
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 没有可以复用的,那就 创建一个
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          // 然后再对比 input 的type 和 tag 等属性,一样的话,吧老节点 对应的内容制空,复用节点
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 虽然 key 相等,但是节点类型不同,创建新节点
            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)
    }
  }

所以 react 和 vue 的 diff 之间的异同?

  1. 当 key 为 null,也就是没有设置 key 的时候,两个都是当作 相同的节点来进行的

  2. react 和 vue 中对应 两个节点是否 可以复用 有着不同的判断

  3. react 中的 复用,指的是 key 相等,然后 type 相等,这里的 type 是在创建的时候,根据节点类型 设置的 

  4. 而 vue 的 复用,指的是 key 相等,节点的 tag 类型相等,input 的 type 相等

  5. 而 vue diff 中的 优化,指的就是 对 排头排尾 的比较了,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章