Vue2源码,虚拟DOM篇-Diff

587 阅读4分钟

Diff

VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在,然后更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的

Diff 算法就是虚拟DOM的核心所在

所谓旧的VNode(即oldVNode)就是数据变化之前视图所对应的虚拟DOM节点,而新的VNode是数据变化之后将要渲染的新的视图所对应的虚拟DOM节点,所以我们要以生成的新的VNode为基准,对比旧的oldVNode,如果新的VNode上有的节点而旧的oldVNode上没有,那么就在旧的oldVNode上加上去;如果新的VNode上没有的节点而旧的oldVNode上有,那么就在旧的oldVNode上去掉;如果某些节点在新的VNode和旧的oldVNode上都有,那么就以新的VNode为准,更新旧的oldVNode,从而让新旧VNode相同

总结:以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样,这就是patch过程要干的事

  • 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  • 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  • 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode

静态节点

<p>我是不会变化的文字</p>

上面这个节点里面只包含了纯文字,没有任何可变的变量,这也就是说,不管数据再怎么变化,只要这个节点第一次渲染了,那么它以后就永远不会发生变化,这是因为它不包含任何变量,所以数据发生任何变化都与它无关。我们把这种节点称之为静态节点。

patch

如果VNodeoldVNode均为静态节点

我们说了,静态节点无论数据发生任何变化都与它无关,所以都为静态节点的话则直接跳过,无需处理。

如果VNode是文本节点

如果VNode是文本节点即表示这个节点内只包含纯文本,那么只需看oldVNode是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode里的文本改成跟VNode的文本一样。如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把它改成文本节点,并且文本内容跟VNode相同。

如果VNode是元素节点

如果VNode是元素节点,则又细分以下两种情况:

  • 该节点包含子节点

    如果新的节点内包含了子节点,那么此时要看旧的节点是否包含子节点,如果旧的节点里也包含了子节点,那就需要递归对比更新子节点;如果旧的节点里不包含子节点,那么这个旧节点有可能是空节点或者是文本节点,如果旧的节点是空节点就把新的节点里的子节点创建一份然后插入到旧的节点里面,如果旧的节点是文本节点,则把文本清空,然后把新的节点里的子节点创建一份然后插入到旧的节点里面。

  • 该节点不包含子节点

    如果该节点不包含子节点,同时它又不是文本节点,那就说明该节点是个空节点,那就好办了,不管旧节点之前里面都有啥,直接清空即可。

  /* 
    这里就是大名鼎鼎的patch,
    更新节点
  */
  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    /* 如果完全相等,就直接return */
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    // 首先取一下真实DOM
    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
    }

    // // 是否都是静态节点
    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)
    }
    // 判断有没有text属性,如果没有
    if (isUndef(vnode.text)) {
      // 是否都存在子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 如果都存在子节点,判断子节点是否相同,不同则更新子节点
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) { // 如果只有VNode存在子节点

        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        /* 
            判断oldVNode是否有文本
            如果有的话,先清空文本,再把子节点添加到DOM中
        */
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 把子节点添加到DOM中
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /* 
          oldVNode 有子节点,但是VNode没有子节点
          那就清空DOM中的子节点
        */
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /* 
          VNode和oldVNode都没有子节点,但是oldVnode有文本
          清空oldnode文本
        */
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // vnode的text属性与oldVnode的text属性是否相同?
      // 如果不相同,VNode的text直接替换DOM的文本
      // 
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

更新子节点

这个需要单独拿出来说

如果都存在子节点,并且子节点不相同,那么就需要updateChildren函数

updateChildren

/* 
    当VNode和oldVNode 都有子节点,并且不相等,执行这个函数
    这里就是那个diff算法了
  */
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 /* oldChildren开始索引 */
    // // newChildren开始索引
    let newStartIdx = 0 /*  */
    let oldEndIdx = oldCh.length - 1 /* oldChildren结束索引 */
    // newChildren结束索引
    let newEndIdx = newCh.length - 1
    // oldChildren中所有未处理节点中的第一个
    let oldStartVnode = oldCh[0]
    //  oldChildren中所有未处理节点中的最后一个
    let oldEndVnode = oldCh[oldEndIdx]
    // newChildren中所有未处理节点中的第一个
    let newStartVnode = newCh[0]
    // newChildren中所有未处理节点中的最后一个
    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)
    }
    /* 
      oldStartIdx : 旧虚拟节点的开始index
      oldEndIdx: 旧虚拟节点的END的index
      newStartIdx: 新虚拟节点的开始index
      newEndIdx: 新虚拟节点的开始index
    */
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        /* 如果oldStartVnode 是undefined的话,那么指针向后移动 */
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        /* oldEndVnode 如果是undefined的话,指针向前移动 */
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        /* 新前和旧前是同一个VNode,那么更新子节点,指针后移 */
        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 {
        // 如果不属于上面四种情况,就进行常规的循环对比patch
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        /* 这里不是循环寻找,而是以map的形式 */
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        if (isUndef(idxInOld)) { 
          // 如果在oldChildren 里面找不到newChildren 里的子节点
          // 新增节点并插入到合适的位置
          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)
          }
        }
        // newStartIdx 指针往后移动
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /* 
        如果 oldChildren 先循环完
        把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
       */
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      /* 
        如果newChildren 先循环完,那么直接删除oldChildren 里面剩余的
        把[oldStartIdx, oldEndIdx]之间的所有节点都删除
      */
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }