1

原文链接我的blog,欢迎STAR。

接着上一篇,我们继续来讲Vue的Virtual Dom diff 算法中的patchVnode方法,以及核心updateChildren方法。


在上篇中,我们谈到,当vnode不为真实节点,且vnode与oldVnode为同一节点时,会调用patchVnode方法。
我们直接从源码上进行分析:

  // patchVnode()有四个参数
  // oldVnode: 旧的虚拟节点
  // vnode: 新的虚拟节点
  // insertedVnodeQueue:  存在于整个patch中,用于收集patch中插入的vnode;
  // removeOnly: 这个在源码里有提到,removeOnly is a special flag used only by<transition-group>也就是说是特殊的flag,用于transition-group组件。
  
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果oldVnode与vnode为同一引用, 不进行任何处理。
    if (oldVnode === vnode) {
      return
    }
    
    // 如果不为同一引用,那说用新的vnode创建了。
    // 如果vnode, oldVnode都为静态节点,且vnode.key === oldVnode.key相等时,当vnode为克隆节点,或者vnode有v-once指令时,只需把oldVnode对应的真实dom,以及组件实例都复制到vnode上。
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
      vnode.elm = oldVnode.elm
      vnode.componentInstance = oldVnode.componentInstance
      return

    // 在进行下一步操作之前会调用prepatch hook,但是这个是vnode在data里定义的prepatch hook,并不是全局定义的prepatch hook
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    
    // 让vnode引用到现在的真实DOM,当elm修改的时候,会同步修改vnode.elm
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    
    // 我们先patchVnode, 方法就是先调用全局的update hook
    // 然后调用data里定义的update hook
    if (isDef(data) && isPatchable(vnode)) {
      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)
    }
    
    // 如果vnode.text未定义
    // 这里有个值得注意的地方,具有text属性的vnode不应该具备有children
    // 对于<p>abc<i>123</i></p>的写法应该是
    // h('p', ['abc', h('i', '123')])
    // 而不是, h('p', 'abc', [h('i', '123')])
    // 因此,对text存在与否的情况需单独拿出来分析
    if (isUndef(vnode.text)) {

      // 如果oldVnode与vnode都存在children
      if (isDef(oldCh) && isDef(ch)) {
        
        // 如果两个children 不相同,调用updateChildren()方法更新子节点的操作。(接下来将讲解)
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 如果只有vnode.children 存在
        // 当oldVnode.text不为空,vnode.text未定义时,清空elm.textContent
        // 添加vnode.children
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 如果只有oldVnode.children存在,移除oldVnode.children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 同上,如果oldVnode.text存在,vnode.text不存在,清空elm.textContent
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
        
      // 如果vnode.text存在(vnode是一个text node),且不等于oldVnode.text
      // 更新elm.textContent
      nodeOps.setTextContent(elm, vnode.text)
    }
    
    // 最后再调用 postpatch hook。
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

接着说重点 当oldVnode.children与vnode.children都存在,且不相同时调用的updateChildren()方法, 同样的,咱们从源码上分析:

 // updateChildren(),有五个参数
 // parentElm: oldVnode.elm 的引用
 // oldCh, newCh: 分别是上面分析中的oldVnode.children, vnode.children
 // insertedVnodeQueue, removeOnly 请参考上面。
 
 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, elmToMove, 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
    
    // 遍历过程共有5种情况
    // 比较判断的依据是,sameVnode(),值不得值得比较。
    // key,tag(当前节点标签名),isComment(是否是注释节点)
    // data,节点的数据对象是否都存在或都不存在
    // (a, b)=> {
    //   return (
    //        a.key === b.key &&
    //        a.tag === b.tag &&
    //        a.isComment === b.isComment &&
    //        isDef(a.data) === isDef(b.data) &&
    //        sameInput(a, b)
    //    )
    // }
    // 当oldStartIndex > oldEndIdx 或者 newStartIndex > newEndIdx, 停止遍历。
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        
      // 对于vnode.key的比较,会把oldVnode = null
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx]

        // 同上
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
      
        // 第一种情况:
        // 从oldCh与newCh的第一个开始,逐步往后遍历。
        // 如果oldStartVnode与newStartVnode值得比较,
        // 执行pathchVnode()方法
        // oldStartVnode, newStartVnode相对位置不变。
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        
        // 第二种情况:
        // 从oldCh与newCh的最后一个开始,逐步往前遍历。
        // 如果oldEndVnode,newEndVnode值得比较
        // 执行pathchVnode()
        // oldEndVnode, newEndVnode相对位置不变
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        
        // 第三种情况:
        // 从oldCh的第一个,newCh的最后一个开始,oldCh往后,newCh往前遍历,
        // 如果oldStartVnode与newEndVnode值得比较
        // 此时需要把oldStartVnode放到oldEndVnode后面
        // oldCh往后,newCh往前
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        
        // 第四种情况:
        // 从oldCh的最后一个,newCh的第一个,oldCh往前,newCh往后,遍历。
        // 如果oldEndVnode与newStartVnode值得比较
        // 此时需要把oldEndVnode放到oldStartVnode前边
        // oldCh往前,newCh往后
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
      
        // 第五种情况:
        // 使用key比较
        // 首先会调用createKytoOldIdx()方法,产生一个key-index对象列表
        // 然后根据这个表来进行更改
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        
        // 如果newStartVnode.key存在,根据key来找到对应的index,命名为idxInOld 
        // 如果不存在,设置为null
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) {
        
          // 如果idxInOld不存在时,此时是一个新的vnode
          // 将这个vnode插入到oldStartVnode.elm 的前边
          // 把newStartVnode设置为下一个节点
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
            
          // 如果idxInOld存在时,那么对应的oldVnode存在
          // 根据index,找到oldVnode对应的children
          elmToMove = oldCh[idxInOld]

          // 如果不是生产环境,且elmToMove不存在
          // 此时因为idxInOld已经存在,而oldCh[idxInOld]不存在
          // 只有可能keys重复了
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          
          // 如果根据vnode.key找出的elmToMove与newStartVnode值得比较比较
          // patchVnode这两个节点
          // 之后,需要把这个child设置为undefined
          // 同时需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影响接下来的遍历。
          if (sameVnode(elmToMove, newStartVnode)) {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]**重点内容**
          } else {
            // same key but different element. treat as new element
            // 如果不值得比较,此时key已经相同,说明是tag不同,或者其他不同,此时创建一个新节点
            // 将这个vnode插入到oldStartVnode.elm 的前边
            // 把newStartVnode设置为下一个节点
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    
    // 遍历完成之后,存在两种情况
    // 如果 oldStartIdx > oldEndIdx, 即oldCh先遍历完
    // 位于 newStartIdx与newEndIdx之间的节点都可认为是新的节点
    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, 即newCh先遍历完
      // 此时,位于oldStartIdx与oldEndIdx之间的节点已经不存在了
      // 调用removeVnodes()方法移除节点。
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

直接在源码上分析,可能有点乱,总结一下:

patchVnode共有以下情况:

  • 如果oldVnodevnode引用完全一致,则可以认为没有变化,无需进行任何操作。

  • 如果vnode, oldVnode都为静态节点,且vnode.key === oldVnode.key相等时,当vnode为克隆节点,或者vnodev-once指令时,只需把oldVnode对应的真实dom,以及组件实例都复制到vnode上。

  • 如果vnode不是text node:

    • 如果vnode.childrenoldVnode.children都存在,调用updateChildren()方法。

    • vnode.children存在,oldVnode.children不存在时,添加vnode.children

    • vnode.children不存在,oldVnode.children存在时,需要移除oldVnode.children

    • 当两者的children都不存在时,如果oldVnodetext node,则需清空elm.textContent

  • 如果vnodetext node,改变elm.textContent

patchVnode有一个值得注意的地方是,vdom中规定,具有text属性的vnode不应该具备children,因此需把text node单独拿出来分析。
_

updateChildren()方法共有5种比较方式,前四种无key的情况,后一种为有key的情况,当oldStartIdx > oldEndIdx或者newStartIdx > newOldStartIdx的时候停止遍历。

引用推荐的那篇文章图:

遍历示意图

  • 第一种比较方式从oldChnewCh各自第一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法进行比较, 同时将oldChnewCh的下一个vnode分别设为oldStartVnodenewStartVnode(比较的相对位置不变), startVnode既是开始比较的vnode

  • 第二种比较方式从oldChnewCh各自最后一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法进行比较,同时将oldChnewCh的上一个vnode分别设置为oldEndVnodenewEndVnode(比较的相对位置不变),, endVnode既是结束比较的vnode

  • 第三种比较方式,从oldCh的第一个vnodenewCh的最后一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法比较,同时将oldCh的下一个vnode设置为oldStartVnode,将newCh的上一个vnode设置为newEndVnode,并且此时说明oldStartVnode.elm向右移动,并且已经移动到oldEndVnode.elm的后边了,调用相应的方法移动位置。

  • 第四种比较方式,从oldCh的最后一个vnodenewCh的第一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法比较,同时将oldCh的上一个vnode设置为oldEndVnode,将newCh的上一个vnode设置为newStartVnode,并且此时说明oldEndVnode.elm向左移动,并且已经移动到oldStartVnode.elm的前边了,调用相应的方法移动位置。

  • 第五种,使用key比较,先会产生一个key-index表,然后判断vnode.key存在与否?

    • 如果不存在,是一个新的vnode,将这个vnode插入到oldStartVnode.elm 的前边,并且把newStartVnode设置为下一个节点。

    • 如果存在,那么对应的oldVnode应该存在,此时可以根据key来找到对应的vnode,然后判断这个vnodenewStartVnode是否值得比较?

      • 当值得比较时,调用patchVnode,并且需要把这个child设置为undefined,同时需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影响接下来的遍历。

      • 如果不值得比较,此时key已经相同,说明是tag不同,或者其他不同,此时创建一个新节点将这个vnode插入到oldStartVnode.elm 的前边。

遍历完成后,如果oldCh先遍历完,位于newStartIdx与newEndIdx之间的节点都可认为是新的节点,调用相应的方法插入节点。如果newCh先遍历完,此时,位于oldStartIdx与oldEndIdx之间的节点已经不存在了,调用removeVnodes()方法移除节点。


结语

码完这篇历时四天的文章,我的身心是崩溃的,期间查阅了相当多的资料,来确保表达的准确性(当然其中还有一些错误的地方,如有发现,请指出。),推荐从关于一些Vue的文章。(2),开始阅读,算是逐渐深入吧,从render,template,el => vnode => diff算法。如果各位同学喜欢,麻烦点个赞。谢谢。

完。


三毛
793 声望68 粉丝

读书,码农,民谣。