最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue源码分析(十二)

    正文概述 掘金(cherish553)   2020-12-16   514

    八、组件更新

    当完成了首次的渲染之后,组件的响应式数据发生了更新,再次触发了渲染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做的一层优化处理


    起源地下载网 » vue源码分析(十二)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元