vue源码阅读(六):diff 算法

前言

vue中,首先是将模板编译成虚拟DOM,而后再将虚拟DOM转为真实的DOM。当咱们的页面有更新时,仅仅是改变了很小的一部分,要去替换总体旧的DOM的话,势必会影响性能和用户体验的。因此vue中使用diff算法来优化DOM的更新渲染。vue

createPatchFunction

在将虚拟DOM转为真实DOM中,有一个很重要的函数,就是createPatchFunction。其中又有一段很重要的代码。node

return function patch(oldVnode, vnode, hydrating, removeOnly) {
    ...
    // 没有旧节点,直接生成新节点
    if (isUndef(oldVnode)) {
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 先用 sameVnode 判断新旧节点是否同样,同样的话,
      // 就用 patchVnode 找不同的地方,好比说子节点之类的
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 建立新节点
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // 销毁旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    ...
    return vnode.elm
  }
复制代码

这里分为三种状况,算法

  • 一、没有旧节点:直接建立新节点
  • 二、有旧节点,可是和新节点不同:建立新节点,删除旧节点
  • 三、有旧节点,可是和新节点同样:进入patchVnode

前两种状况,以前的文章中,已经讲过。接下来,咱们就详细看看patchVnode数组

patchVnode

function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) {
    // ...
    // 新旧节点彻底一致,直接返回
    if (oldVnode === vnode) {
      return
    }
    // 将旧节点上的 DOM,添加到新节点上。以后新旧节点如有不一致,直接修改 elm 便可
    const elm = vnode.elm = oldVnode.elm

    const oldCh = oldVnode.children
    const ch = vnode.children
    // 新节点不是文本节点
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新旧节点都存在子元素
        if (oldCh !== ch) 
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 只有新节点存在子元素,先清空节点上的文本,而后将子元素建立为真实 DOM 插入
        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) {
      // 新旧节点上的文本节点不一致,更新新节点上的 DOM
      nodeOps.setTextContent(elm, vnode.text)
    }
    //...
  }
复制代码

patchVnode主要作了两件事,函数

  • 一、判断新节点是不是文本节点,若是是文本节点,就须要判断与旧节点上的文本节点是否一致。不一致的时候,就须要更新节点上的文本。性能

  • 二、是当新节点不是文本节点时候,就须要对新旧节点的子元素进行判断了。这里有四种状况:优化

    • 新旧节点都有children:使用updateChildren比较两个children
    • 只有新节点有children:清空旧节点上的文本,而后将新节点建立为真实DOM后,插入到父节点。
    • 只有旧节点有children:删除节点上的children
    • 当只有旧节点上有文本时:新节点上没有,直接清空便可。

updateChildren

重点看下updateChildren这个函数,spa

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

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 若是旧节点中开始的节点是 undefined,开始节点下标就后移一位
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 若是旧节点结束节点是 undefined,结束节点下标就迁移一位
        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 {
        // 不然,将每一个旧节点的 key 值组成一个对应的 map 表,而后判断新节点的 key 是否在 map 表中
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // idxInOld 是在旧节点列表中,与新节点相同的旧节点位置
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key] // key 值比较
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // sameVnode 进行比较
        if (isUndef(idxInOld)) { // New element
          // 若是 key 不在 map 表中,则建立新节点
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 若在,则判断该 key 值对应的旧节点与新节点是不是相同的节点
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 若该 key 值对应的旧节点与新节点是相同的节点,则比较他们的子节点
            // 同时将该 key 值对应的节点插入到旧开始节点以前
            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)
          }
        }
        // key 值判断以后,新开始节点后移一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      // 若是旧节点列表先处理完,则将剩余的新节点建立为真实 DOM 插入
      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)
    }
  }
复制代码

能够看出updateChildren主要的做用是比较新旧子节点,分为5种状况:3d

  • 一、旧开始节点 == 新开始节点
    若旧开始节点与新开始节点相等时,说明旧开始节点的位置是对的,不须要更新该节点。以后是将旧开始节点和新开始节点的下标后移一位。

  • 二、旧结束节点 == 新结束节点
    若旧结束节点与新结束节点相等,说明旧节点的位置是对的,不须要更新该节点。以后是将旧结束节点和新结束节点的下标前移一位。

  • 三、旧开始节点 == 新结束节点
    若旧开始节点与新结束节点相等,说明旧开始节点的位置不对了,须要移动到oldEndVnode后面。而后将旧开始节点的下标后移一位,新结束节点的下标前移一位。

  • 四、旧结束节点 == 新开始节点
    若旧结束节点与新开始节点相等,说明旧结束节点须要移动到oldStartVnode前面。而后将旧结束节点前移一位,新开始节点位置后移一位。

  • 五、key 值查找code

    当前面四种比较都不行的时候,则会去经过key值进行查找。查找时候是当前的新节点,去遍历旧节点数组,找到相同的旧节点,而后将其移到 oldStartVnode 前面。大体流程是:

处理剩余节点

接着就是处理余下的新旧节点。有两种状况:

  • 一、新节处理完了,旧节点还有剩余
    将剩余的旧节点,逐个删除便可。
// 删除剩余的旧节点
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  
  function removeVnodes(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)
        }
      }
    }
  }
复制代码
  • 二、新节点有剩余,旧节点处理完了 逐个建立剩余的新节点。有个问题是,将剩余的新节点建立好后,插入到哪里呢?
// 将剩余的新节点建立为真实的 DOM 插入
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    }
  }
复制代码

咱们能够看到,refElm 是获取新节点最后一个节点。
若是refElm存在的话,说明最后一个节点以前被处理过,那么剩余的新节点插入到refElm前面便可。
若是refElm不存在,则将剩余的新节点插入到父节点孩子的末尾。

本文到此也就结束了,相信你们也对 vue 中 diff 算法有必定了解。结束的结束,有个小问题,你们以为 v-for 中 key值的做用是什么呢?

相关文章
相关标签/搜索