Diff算法

144 阅读3分钟

两个概念:

  • Virtual Dom(虚拟dom)
  • diff算法

虚拟dom

为什么需要虚拟dom

每次更新真实dom的开销非常大,如果修改数据之后直接渲染到真实dom上会引起页面的重绘和回流。 虚拟dom可以在结点改变之后和原结点比较,如果发现有变化就会修改真实dom,旧结点会被虚拟dom的新节点替换。

重绘:元素外观、颜色等不影响布局的属性改变;

回流:元素的布局、位置、尺寸改变

什么是虚拟dom

虚拟dom是类似与对象结构的存储的一种数据

dom:

     <div>
        <span>*</span>
    </div>

对应的虚拟dom:

    {
        tag: 'div',
        children: [
            { tag: 'span', text: '*' }
        ]
    }

diff算法 (Vue2)

比较方式

同级比较,不会跨级比较,如果父级结点一样就会比较子节点


    <div>
        <span>*</span>
    </div>
    
     <div>
        <p>1</p>
    </div>
    

div和div比较,span和p比较

image.png

比较流程

image.png

patch

首先会判断新节点是否为空:空的话会卸载老节点, 判断老节点是否为空:空的话会创建新节点

工具函数:

  • isUndef 判断是否未定义
  • isDef 判断是否定义
  • invokeDestroyHook 卸载结点
  • createElm 创建结点
function patch(oldVnode, vnode) {
  // 判断新的vnode是否为空
  if (isUndef(vnode)) {
    // 如果老的vnode不为空 卸载所有的老vnode
    if (isDef(oldVnode)) {
      invokeDestroyHook(oldVnode)
    }
    return
  }
  
  // 如果老节点不存在,直接创建新节点
  if (isUndef(oldVnode)) {
    createElm(vnode)
  } else {
    // 新老节点的type和key相同,进行patchVnode更新工作
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode)
    } else {
      // 拿到 oldVnode 的父节点
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
      // 插在老节点的弟弟结点的前面 insertBefore
      createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))

      // 如果旧节点还存在,就删掉旧节点
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        // 否则直接卸载 oldVnode
        invokeDestroyHook(oldVnode)
      }
    }
  }
  // 返回最新 vnode 的 elm ,也就是真实的 dom节点
  return vnode.elm
}

sameVnode

判断条件:

  • key相同
  • 标签名相同
  • 注释节点标识相同(都是 或者 都不是)
  • data值相同(都有值 或者 都没有值) key
function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

patchVnode

当两个结点都相同时会执行该方法,判断子节点是否相同

patchVnode (oldVnode, vnode) {
    // 获取真实dom
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    
    // 如果两个节点都指向同一个对象直接return
    if (oldVnode === vnode) return
    
    // 比较两个节点的文本节点
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        // 不一样就把vnode的文本节点设置给el
        api.setTextContent(el, vnode.text)
    }else {
        // 一样
        if (oldCh && ch && oldCh !== ch) {
            // 子节点不一样
            updateChildren(el, oldCh, ch)
        }else if (ch){
            // oldv不存在子节点 则创建新节点
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            // v 不存在子节点 则移除oldv的子节点
            api.removeChildren(el)
        }
    }
}

updateChildren

当两个节点的子节点不同时会调用该方法

updateChildren (parentElm, oldCh, newCh) {
  let oldStartIdx = 0, 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
  let idxInOld
  let elmToMove
  let before
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 对于vnode.key的比较,会把oldVnode = null
    // 判断节点是否为空,空的话索引值从两边向中间移动
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx]
    }else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx]
    }else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx]
    }else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx]
    }
    // 不为空时判断两个节点是否相同,判断完成之后索引值向中间移动
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode)
      api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode)
      api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }

    // 使用key时的比较
    else {
      if (oldKeyToIdx === undefined) {
        // oldvnodeCh含有key值的生成key字典
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
      }
      // vnch中节点key 去字典表中查找 { keyName: oldStartIdx, keyName1: oldStartIdx + 1, ... keyNameEnd: oldEndIdx }
      idxInOld = oldKeyToIdx[newStartVnode.key]
      if (!idxInOld) {
        // 查不到则插入,索引++
        api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
        newStartVnode = newCh[++newStartIdx]
      }
      else {
        elmToMove = oldCh[idxInOld]
        // 判断节点是否相同
        if (elmToMove.sel !== newStartVnode.sel) {
          api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
        }else {
          // 比较子节点
          patchVnode(elmToMove, newStartVnode)
          oldCh[idxInOld] = null
          api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
  }
  if (oldStartIdx > oldEndIdx) {
    // 如果oldv 索引超出范围,则newv中剩下的节点添加到真实dom中,索引为原来的索引值
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
  }else if (newStartIdx > newEndIdx) {
    // 移除oldv中剩余的子节点
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}
图解

无key的情况下:

image.png

有key的情况下:

在key的index表中查找对应的ov的key

  • 没找到: 则直接新增v节点
  • 找到:将 v 和 对应索引值的 ov节点比较
    • 不同:直接新增v子节点
    • 相同:清空ov中的该子节点,比较两子节点中的子节点,并且将ov子节点移动到当前未比较老节点首部

vue3 和 react的diff算法略有不同

参考资料:www.cnblogs.com/wind-lanyan…