vue:虚拟dom的patch

  • Post author:
  • Post category:vue


源码目录:

src/core/vdom/patch.js

  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

       const canMove = !removeOnly

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 开始索引大于结束索引,进不了
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode已经被移走了。
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx] // 索引加1。是去对比下一个节点。比如之前start=a[0],那现在start=a[1],改变start的值后再去对比start这个vnode
        newStartVnode = newCh[++newStartIdx]
         
      } else if (sameVnode(oldEndVnode, newEndVnode)) { 
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// 把节点b移到树的最右边
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
          
      } else if (sameVnode(oldEndVnode, newStartVnode)) {   old.end.d=new.start.d
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)// Vnode moved left,把d移到c的左边。=old.start->old.end
        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)) { 
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 创建新节点,后面执行了nodeOps.insertBefore(parent, elm, ref)
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            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)
          }
        }
        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(parentElm, oldCh, oldStartIdx, oldEndIdx) // 删除旧的c,removeNode(ch.elm)

    }
  }
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

/**
     * 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树,在这个过程
     * @param oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updateChildren方法对子节点做更新,
     * @param vnode
     * @param insertedVnodeQueue
     * @param removeOnly
     */
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }

    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
    }

      // 用于静态树的重用元素。
        // 注意,如果vnode是克隆的,我们只做这个。
        // 如果新节点不是克隆的,则表示呈现函数。
        // 由热重加载api重新设置,我们需要进行适当的重新渲染。
    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
    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)) {
      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)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, 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)
    }
  }

function insertBefore (parentNode, newNode, referenceNode) {
  parentNode.insertBefore(newNode, referenceNode);
}

/**
     *
     * @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法,
     * @param insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法
     * @param parentElm
     * @param refElm
     * @param nested
     */

     let inPre = 0
  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    vnode.isRootInsert = !nested // 过渡进入检查
    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) {
          inPre++
        }
        if (
          !inPre &&
          !vnode.ns &&
          !(
            config.ignoredElements.length &&
            config.ignoredElements.some(ignore => {
              return isRegExp(ignore)
                ? ignore.test(tag)
                : ignore === tag
            })
          ) &&
          config.isUnknownElement(tag)
        ) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      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 {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        inPre--
      }
    } 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)
    }
  }
function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (ref.parentNode === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }

function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (isDef(ch)) {
        if (isDef(ch.tag)) {
          removeAndInvokeRemoveHook(ch)
          invokeDestroyHook(ch)
        } else { // Text node
          removeNode(ch.elm)
        }
      }
    }
  }


updateChildren

方法主要通过

while

循环去对比2棵树的子节点来更新

dom

,通过对比新的来改变旧的,以达到新旧统一的目的。

通过一个例子来模拟一下:

假设有新旧2棵树,树中的子节点分别为

a,b,c,d

等表示,不同的代号代表不同的

vnode

,如:


clipboard.png

在设置好状态后,我们开始第一遍比较,此时

oldStartVnode=a,newStartVnode=a;

命中了

sameVnode(oldStartVnode,newStartVnode)

逻辑,则直接调用

patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)

方法更新节点

a

,接着把

oldStartIdx



newStartIdx

索引分别+1,如图:


clipboard.png

更新完节点

a

后,我们开始第2遍比较,此时

oldStartVnode=b,newEndVnode=b;

命中了

sameVnode(oldStartVnode,newEndVnode)

逻辑,则调用

patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)

方法更新节点

b

,接着调用

canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))

,把节点

b

移到树的最右边,最后把

oldStartIdx

索引+1,

newEndIdx

索引-1,如图:


clipboard.png

更新完节点

b

后,我们开始第三遍比较,此时

oldEndVnode=d,newStartVnode=d;

命中了

sameVnode(oldEndVnode, newStartVnode)

逻辑,则调用

patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)

方法更新节点

d

,接着调用

canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)

,把

d

移到

c

的左边。最后把

oldEndIdx

索引-1,

newStartIdx

索引+1,如图:


clipboard.png

更新完

d

后,我们开始第4遍比较,此时

newStartVnode=e

,节点

e

在旧树里是没有的,因此应该被作为一个新的元素插入,调用

createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

,后面执行了

nodeOps.insertBefore(parent, elm, ref)

方法把

e

插入到

c

之前,接着把

newStartIdx

索引+1,如图:


clipboard.png

插入节点

e

后,我们可以看到

newStartIdx

已经大于

newEndIdx

了,

while

循环已经完毕。接着调用

removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)

删除旧的

c

,最终如图:


clipboard.png


updateChildren

通过以上几步操作完成了旧树子节点的更新,实际上只用了比较小的

dom

操作,在性能上有所提升,并且当子节点越复杂,这种提升效果越明显。

vnode

通过

patch

方法生成

dom

后,会调用

mounted hook

,至此,整个

vue

实例就创建完成了,当这个

vue

实例的

watcher

观察到数据变化时,会两次调用

render

方法生成新的

vnode

,接着调用

patch

方法对比新旧

vnode

来更新

dom

.