3-2-24-Vue.js 源码阅读-patch

Vue 中的虚拟 DOM - update

Vue 中的 update 最终更新视图调用的方法是通过高阶函数 createPatchFunction 创建的一个 patch 函数,下面分析 patch 函数的执行过程。

// 函数柯里化,让一个函数返回一个函数
// createPatchFunction({ nodeOps, modules })传入平台相关的两个参数

// core 中的 createPatchFunction (backend), const { modules, nodeOps } = backend
// core 中方法和平台无关,传入两个参数后,可以在上面的函数中使用这两个参数
return function patch (oldVnode, vnode, hydrating, removeOnly) {
  // 新的 vnode 不存在
  if (isUndef(vnode)) {
    // 老的 vnode 存在,执行 Destroy 钩子函数
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  // 存储新插入的 vnode 节点的队列,为了将来把这些 vnode 节点对应的 DOM 元素挂载在 DOM 树上之后回去触发这些 vnode 的 insert 的钩子函数
  const insertedVnodeQueue = []

  // 老的 vnode 不存在
  if (isUndef(oldVnode)) {
    // 调用组件的 $mount 的时候没有传参数,所以只是创建了一个DOM节点并没有将其挂载到真实 DOM 上(只是在内存中保存不显示)
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    // 创建新的 vnode
    createElm(vnode, insertedVnodeQueue)
  } else {
    // 新的和老的 vnode 都存在,更新
    const isRealElement = isDef(oldVnode.nodeType)
    // 判断参数1是否是真实 DOM ,如果不是真实 DOM
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 更新操作,diff 算法
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 第一个参数是真实 DOM,创建 vnode
      // 初始化
      if (isRealElement) {
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      // 创建 DOM 节点,并挂载到 oldVnode 父元素
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // 移除 oldVnode 并触发响应的钩子函数
      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  // 触发 vnode 的 insert 钩子函数
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

createElm

createElm 的核心作用就是将 vnode 转换成真实 DOM 并挂载到 DOM 树上。

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // 元素节点
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        // 如果是一个未知的标签(自定义标签)
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    // 是否有命名空间(svg)
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // in Weex, the default insertion order is parent-first.
      // List items can be optimized to use children-first insertion
      // with append="tree".
      const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
      if (!appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
      createChildren(vnode, children, insertedVnodeQueue)
      if (appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
    } else {
      // 将 vnode 中的所有的子节点转换成 DOM 对象
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        // 触发 create 钩子函数
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // 插入节点
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    // 注释节点
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

patchVnode

对比新旧 vnode 找到差异,更新真实 DOM。即执行 diff 算法。

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  // 如果新旧 vnode 都是静态的,那么只需要替换 componentInstance
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  // 执行用户传入的 prepatch 钩子函数
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    // 调用 cbs 中的钩子函数,操作节点的属性/样式/事件...
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    // 用户的自定义钩子
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  // 新节点没有文本
  if (isUndef(vnode.text)) {
    // 新节点和老节点都有子节点
    // 对子节点进行 diff 操作,调用 updateChildren
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // 新节点有子节点,老节点没有
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      // 清空老节点 DOM 的文本内容,然后为当前 DOM 节点加入子节点
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 老节点有子节点,新节点没有子节点
      // 删除老节点中的子节点,触发 remove 和 destroy 钩子函数
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 老节点有文本,新节点没有文本
      // 清空老节点的文本内容
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // 新老节点都有文本节点
    // 修改文本节点
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

updateChildren

在 patchVnode 中,当新老节点都有子节点,并且是 sameVnode 的时候,会调用 updateChildren ,对比新老子节点找到差异更新到 DOM 树。如果子节点没有发生变化会重用节点。

// 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
  // 是否有相同的key
  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }
  // diff 算法
  // 当新节点和旧节点都没有遍历完成
  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)) {
      // oldStartVnode 和 newStartVnode 相同(sameVnode)
      // 直接将该 vnode 节点进行 patchVnode
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // 获取下一组开始节点
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 直接将节点进行 patchVnode
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // 获取下一组结束节点
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // oldStartVnode 和 newEndVnode 相同(sameVnode)
      // 进行 patchVnode ,把 oldStartVnode 移到最后
      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
      // oldEndVnode 和 newStartVnode 相同(sameVnode)
      // 进行 patchVnode,把 oldEndVnode 移到最前面
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 以上四种情况都不满足
      // newStartVnode 依次和旧的节点比较

      // 从新的节点开头获取一个,去老节点中查找相同节点
      // 先找新开始节点的 key 和老节点相同的索引,如果没有找到再通过 sameVnode 找
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      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]
        // 如果使用 newStartNode 找到相同的老节点
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // 执行 patchNode,并将找到的旧节点移动到最前面
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // 如果 key 相同,但是是不同的元素,创建新元素
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // 当循环结束后,oldStartIdx > oldEndIdx,旧节点遍历完,但是新节点还没有
  if (oldStartIdx > oldEndIdx) {
    // 说明新节点比老节点多,把剩下的新节点插入到老节点后面
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    // 当结束时 newStartIdx > newEndIdx ,新节点遍历完,但是旧节点还没有
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值