vue源码分析(十二)

652 阅读3分钟

八、组件更新

当完成了首次的渲染之后,组件的响应式数据发生了更新,再次触发了渲染watcher的getter,也就是调用了 vm._update(vm._render(), hydrating)调用update的这一过程就是组件更新的过程。_update函数首先通过const prevVnode = vm._vnode拿到之前定义的vnode,在之后的逻辑判断中prevVnode为true,接着执行 vm.$el = vm.__patch__(prevVnode, vnode),第一个参数传入之前的vnode,新的参数传入生成的新的vnode。vm.__patch__实际上是patch.js文件当中的patch函数。patch函数中,由于oldVnode定义了,所以本次会执行else逻辑。else逻辑中,首先通过oldVnode.nodeType拿到oldVnode的类型,以此来判断他是否是一个真实的元素节点,如果不是一个真实的元素节点,并且满足sameVnode(oldVnode, vnode)sameVnode函数会尝试拿到传入的两个vnode的key,key在写v-for的时候是非常常见的,如果他们的key相同(如果两者都不写key,则均为undefined,也满足相等的条件),如果满足,他会继续判断如果他们的tag相同,并且都是一个注释节点,并且data都是有定义的,并且是一个相同的input类型,或者如果满足 参数a是一个异步占位符节点并且,a.asyncFactory === b.asyncFactory并且b的执行是正确的,那么就返回true,否则返回false,也就是说samevnode判断两个新旧节点是否相同。如果满足上面的两个条件,那么他会执行patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly),如果新旧节点不同,他会执行else的逻辑,新旧节点不同的情况,他会分三步处理

  • 第一步 创建新的节点 首先通过oldVnode.elm拿到旧的节点,然后通过nodeOps.parentNode(oldElm)拿到旧节点的父级节点,在调用createElm方法创建新的dom节点。执行完这一步,会创建新的节点并进行插入,也就是新的节点和老的节点都存在于页面。
  • 第二步 递归的更新父的占位符节点 首先他会判断是否有vnode.parentvnode.parent_render的最后进行了定义,等于vm.$options._parentVnode也就是父的占位符节点。然后他会执行isPatchable(vnode)isPatchable函数会循环判断是否有vnode.componentInstance,如果有那么代表他是一个组件vnode,那么vnode = vnode.componentInstance._vnode,会继续去找他的渲染vnode,直到找到他的真实渲染节点,如果有父的占位符节点,执行destroy的钩子,然后通过ancestor.elm = vnode.elm对节点进行替换,这样他的父的占位符节点的引用,就指向了新的节点,然后判断如果是一个可挂载节点,那么去执行create等钩子,最后ancestor = ancestor.parentancestor.parent是存在的,那么他还是一个组件,所以会再向上去执行刚才的逻辑,形成递归的的更新父的占位符节点。
  • 第三步 删除旧的节点 通过removeVnodes([oldVnode], 0, 0)对旧的节点进行删除
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ...
    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      ...
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        ...
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        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)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

如果sameVnode(oldVnode, vnode)为true,也就是他们的key相同,以及data相同等,则会执行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

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

    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 {
        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]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            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)
    }
  }
  • 仅仅文本的替换 我们假设这样一个场景
<template>
  <div id='app'>
    <div v-if="flag" @click="flag = false">123</div>
    <div v-else @click="flag = true">444</div>
  </div>
</template>

首次flag为true,当我点击div,触发flag=false,patchVnode函数会先定义oldChch他们分别是旧的vnode和新的vnode的children,首次进入,最初会从<div id='app'>开始进行比较,他的children也就是数组中,有子元素div的vnode,此时的vnode是没有text的,接着判断,oldCh和ch都定义,则会执行updateChildren函数。updateChildren函数会先定义oldStartVnode旧vnode的children的开始节点(旧vnode的children数组的第一项),oldEndVnode旧vnode的children结束节点,newStartVnode新vnode的children开始节点,newEndVnode新vnode的children结束节点,然后他会判断oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,因为我们的#app.div下此时新旧都为div,则这4个值均为0,首先他会判断是否未定义 if (isUndef(oldStartVnode)),此时不满足,接着执行else if (isUndef(oldEndVnode))也不满足,然后他会比较else if (sameVnode(oldStartVnode, newStartVnode)),此时两者是满足samevnode的,会执行patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);,再次执行patchVnode,他的oldVnode和vnode则为这两个新旧div的vnode,同样,他们也会先定义自己的children,也就是两个文本节点123和456的vnode,那么此时div也是没有text的,同时两者都有children,则再次执行updateChildren,再次走入逻辑判断,直到走到之前的samevnode处,接着他会再去执行patchVnode,这次,两个文本vnode的children为undefined,同时vnode.text不为空,接着判断else if (oldVnode.text !== vnode.text)两者的text一个为123,一个为444,满足此条件,接着执行nodeOps.setTextContent(elm, vnode.text)进行文本的替换。此时递归执行完毕,回到最近一次调用updateChildren的场景,也就是两个#app.div下的两个div的updateChildren,执行oldStartVnode = oldCh[++oldStartIdx] oldStartVnode 则为undefined,newStartVnode = newCh[++newStartIdx],newStartVnode也为undefined,最后的两个判断oldStartIdx > oldEndIdxnewStartIdx > newEndIdx均不满足,则结束执行,#app.div的updateChildren同理。

  • 数组的push操作 我们假设这样一个场景
<template>
  <div id="app">
     <ul>
      <li v-for="item in arr" :key="item.id">{{ item.text }}</li>
    </ul>
    <button @click="arr.push({ id: 3, text: 3 })">添加</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      arr: Array.from({ length: 3 }).map((item, index) => ({
        id: index,
        text: index
      }))
    }
  }
}
</script>

此时页面中ul的子元素有3个li,li的key为0,1,2,div里的文本内容也为0,1,2。当点击button,往arr中push一个{ id: 3, text: 3 },进入ul的updateChildren函数,此时oldCh为3个li的vnode节点,而newCh为4个li的的vnode节点。也就是oldEndIdx为3,newEndIdx为4,此时先判断(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)此时为 (0<=2)&&(0<=3)为true,接着判断oldStartVnode是定义的,oldEndVnode也是定义的,当判断else if (sameVnode(oldStartVnode, newStartVnode))是成立的,则执行oldStartVnodenewStartVnodepatchVnode,旧的vnode的key为0的li和新的vnode的key为0的li,他们的文本节点是相同,则当执行带他们的patchVnode的时候,什么也不会执行。接着返回ul的updateChildren函数,执行 oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]也就是oldStartIdx由0变为了1,oldStartVnode指向了第二个li,newStartIdxnewStartVnode同理。接着再次判断 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) 1<=3&&1<=4也成立,也就是说,直到执行到第3个li的对比,两者并无区别,什么操作也没做。对比完第三个li,此时oldStartIdx为3,oldStartVnode为undefined,newStartIdx也为3,newStartVnode为新创建的li key为3的vnode节点。此时while条件中的oldStartIdx <= oldEndIdx不成立,则接着向下执行,判断if (oldStartIdx > oldEndIdx)此时oldStartIdx为3,oldEndIdx为2,则成立,对剩余的接着去执行了addVnodes的插入操作

  • 数组的pop操作 之前的操作都是相同的,当执行完ul的updateChildren的while后,newStartIdx为3,newEndIdx为2,则会执行removeVnodes操作,移除多余的vnode节点。

  • 数组的reverse操作 会执行到判断else if(sameVnode(oldStartVnode, newEndVnode)),接着执行nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))把第一个节点插入到最后,也就是由 0,1,2变为了1,2,0的顺序。接着让oldStartIdx变为了1,oldStartVnode也指向他,newEndIdx变为1,newEndVnode指向他。此时两者都指向了key为1的li,接着就满足了else if (sameVnode(oldStartVnode, newEndVnode)),继续把key为1的li插入到了key为0的li之前,也就是形成了 2,1,0的最终结果

可以看到对于相同节点的diff,会递归向下比较,而不是会直接进行全部的删除和重新创建,这也是vnode做的一层优化处理