Vue源码阅读之diff算法

虚拟dom

  1. 虚拟dom解决了什么问题
  • 首先是正常的一个真实dom拥有的属性非常多,还拥有很多dom操作的方法
  • 其次数据更新的时候如果整个画面重新渲染会带来很大的性能开销,非常慢,而且很多没变化的部分都属于无用功,还不能保存数据更新前的状态
  • 用新数据生成的虚拟dom跟上次旧的虚拟dom做对比,只更新发生变化的部分
  • diff算法也是消耗性能的,所以如果我们知道要修改那个dom,直接手动操作应该是最快的,这样做是为了让我们更关注数据的变化,而不需要关心dom操作
  1. Vue中虚拟dom分类:
注释节点 
文本节点
克隆节点(代表本节点是克隆来的)
元素节点
组件节点 
函数式组件节点
  1. VNode作用,就是将template编译成VNode缓存下来,数据变化生成的VNode与之前的VNode树作对比,将有差异的渲染成真实的Dom插入到视图中,最终一次性视图更新

diff思路

  • 思考:有一组新节点一组旧节点,这个时候数组跟数组间for循环对比时间复杂度是n^2,假如每个节点又有子节点这个复杂度就非常大了,因此不应该做完全对比
  1. 只修改变动的地方不做整个布局的调整
  2. 只做同层比较,不会跨级比较,算法是一个O(n)的算法
  3. 不值得比较的节点直接用新的替换旧的,如果两个节点一样才会去比较其子节点,假如第一层不一样就用新的替换旧的(注意diff算法从来不是最优解,只是一个时间跟节点利用率的平衡方案)

以下代码均在src/core/vdom/patch.js中

patch(oldVnode, vnode) 函数

  1. 根据界面生成Vnode,然后用这个新的Vnode对比旧的Vnode,用新的Vnode去更新真实的dom树
  2. 根据Vnode递归创建真实dom节点,从上到下,先创建子节点后插到父节点上,vnode.elm为真实dom
  3. 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  4. 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  5. 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode

updateChildren子节点算法

Vue源码阅读之diff算法_第1张图片
image
  1. 新前跟旧前作对比,如果节点相同就做patchVnode的操作
  2. 新后跟旧后作对比,如果节点相同就做patchVnode的操作
  3. 旧前跟新后做对比,如果相同就patchVnode这两个节点,然后把真实dom旧前节点移动到oldChilren中所有未处理节点之后,这里做的是真实dom操作


    Vue源码阅读之diff算法_第2张图片
    image
  4. 旧后跟先前做对比,如果相同就patchVnode这两个节点,然后把旧后节点移动到oldChilren中所有未处理节点之前


    Vue源码阅读之diff算法_第3张图片
    image
  5. 假如不属于以上任何一种,就在旧节点中查找当前节点,找不到就创建新节点,找到了假如相同就更新节点,相同key不同元素就当初新节点处理
  6. 如果oldStartIdx > oldEndIdx,把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
  7. 如果newStartIdx > newEndIdx,把[oldStartIdx, oldEndIdx]之间的所有节点都删除
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    debugger
    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 
    // 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)
    }
  }

patchVnode

功能是根据根据新的Vnode去更新旧的VNode,把dom属性,class事件啥的都一一同步,让旧的节点跟新的节点一样

由此编码上的注意点

  • 尽量不要跨层级的修改dom
  • 设置key可以最大化的利用节点,同一层不要设置两个key相同
  • 不同的节点不要设置相同的key
  • 不要盲目相信diff的效率,在必要时可以手工优化

你可能感兴趣的:(Vue源码阅读之diff算法)