Vue中patch实现

423 阅读4分钟

Vue初始化

Vue初始化时会克隆一个新的副本,当该副本所有数据都解析完成,会在老的副本下新插入一个DOM节点,此时会出现两个DOM根节点,然后删除旧的根节点,完成初始化操作。 看下图:

9b499a8e867e8516ad8fb4785e66c50.png

Patch实现

首先进行树级别比较,这个该过程遵循深度优先,同层比较,可能有三种情况:增删改。

  • new VNode不存在就删除。
  • old VNode不存在就新增。
  • 都存在就执行diff执行更新。

a29ff3e3def27b474763af7978a300f.png

patchVnode

比较两个VNode,包含三种类型操作:属性更新、文本更新、子节点更新

具体规则如下:

  • 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
  • 如果新节点有子节点而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点
  • 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点
  • 当新老节点都无子节点的时候,只是文本的替换
updateChildren

updateChildren主要作用是用一种比较高效的方式比对新旧两个VNode的children得出最小操作补丁。执行一个双循环是传统方式,Vue中针对web场景特点做了特别的算法优化。

3bf7c045045d390272d5468da1f012a.png

当新老两组VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢, 当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。

下面是遍历规则: 首先oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉比较,共有4种比较方法。

当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满足sameVnode,直接将该 VNode节点进行patchVnode即可,不需再遍历就完成了一次循环。如下图

3afb1ee9d224c36a5961c48a1266b6c.png

如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode 后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。

e5acbd2c564d4d68b49d4a78d3948b3.png

如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前 面,进行patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前面。

578d030b828d91226c3287d875ae9b4.png

如果以上情况均不符合,则在old VNode中找与newStartVnode相同的节点,若存在执行 patchVnode,同时将elmToMove移动到oldStartIdx对应的DOM的前面。

16bf9d9c6cea638fcc1a67cb0362f63.png

当然也有可能newStartVnode在old VNode节点中找不到一致的sameVnode,这个时候会调用 createElm创建一个新的DOM节点,然后插入到对应的位置。

de5acf32f883d0c1555f064abfe87ce.png 至此循环结束,但是我们还需要处理剩下的节点。

当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说明了新VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes(批量调用createElm接口)。

a99b7b0155bc14dc29de25d9f099f26.png

但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有剩余,需要从文档中删除节点。

b56d1c2acd32c6e4b2b300e842acd2a.png

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.
    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 (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // 新增
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 删除
        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)
    }
  }
// 比较两组孩子
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 设置首尾的4个游标以及响应的节点
    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)
    }
  }

key的作用

ABCDE => AFBCDE 如果无key,只有A会正常替换,剩下的会进行4次更新,1次创建追加 如果有Key,只会对F进行创建和1次插入操作