vue中diff算法解读

401 阅读4分钟

首先说一下vdom和真实dom的区别

首先定义一个真实的结构

<div class="container" id="app">
	<h1>虚拟dom</h1>
	<ul style="color:red">
		<li>第一项</li>
		<li>第二项</li>
		<li>第三项</li>
	</ul>
</div>

对应的js结构---vdom

{
  tag:'div',
  props:{
    id:'app',
    class:'container'
  },
  children:[
    {
      tag:'h1',
      children:'虚拟dom'
    },
    {
      tag:'ul',
      props:{style:'color:red'},
      children:[
        {
          tag:'li',
          children:'第一项'
        },
        {
          tag:'li',
          children:'第二项'
        },
        {
          tag:'li',
          children:'第三项'
        }
      ]
    }

  ]
}

为什么选中vdom

真实的操作dom会造成大量的重流和重绘。造成性能浪费。 虚拟dom是不会立即更新的,会先进行diff算法的比较在更新。所以真实的操作dom的次数相对减少很多。
所以diff算法是什么呢

diff算法

同级间进行比较
首先比较vnode是否相同,通过标签名和key值的比较。

image.png

两者相同(标签和key相同),开始进行下面比较

1.如果新的vnode是text,比较老的,如果老的有children。将旧的children移除。设置成新的text
2.如果新旧node简单的一方有子元素,将旧的node进行增删,并更新视图
3.如果新旧node都有子元素,会进行updateChildren的方法
首先进行四种比较
node的start和oldnode的start对比
node的end和oldnode的end对比
node的start和oldnode的end对比
node的end和oldnode的start对比 生成map的映射。根据old key 记录indexOld.如果indexOld存在,新旧节点相同,就移动旧节点到对应的地方,否则元素不同,就作为新的节点创建。如果indexOld不存在,就创建节点 最后进行遍历,如果老节点遍历完成,则新节点比老节点多将新节点多余的插入老节点,如果新节点遍历完成,则旧节点比新节点多,将多余的节点删除。

下面对应path函数中的方法,具体看下对应的过程。

初始化的时候oldVnode是没有的,所以会创建一个空的vnode,并关联element。
if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
}
接下来如果oldvnode已经存在,判断新旧dom是否相同,进行pathVnode
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
接下来我们看下patchVnode对应的方法
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)
    }
    // 将新的vnode的elm设置成oldvnode的elm,这样patch在更新的时候知道是哪一个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
    }

    // 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)
    }
    //vnode没有text的情况
    if (isUndef(vnode.text)) {
        // 新旧vnode有children的情况
      if (isDef(oldCh) && isDef(ch)) {
        //新旧节点有孩子的情况,执行updateChildren方法
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        // 新vnode有children,旧vnode没有children
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // 旧vnode有text,将旧vnode中的内容删除,同事替换为新的vnode中的内容
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        // 只有旧vnode有children,删除节点内容
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)
        // 新的vnode没内容,旧的是文本,直接删除文本
      } 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)
    }
  }

我们在稍微复习下:

graph TD
A[patchVnode]--- B2[新vnode没有text]
B2 --- C1[新旧vnode有<br/>children的情况]
C1 ---D1[updateChildren]
B2 --- C2[新vnode有children,<br/>旧vnode没有children]
C2 --- D2[旧vnode有文本,<br/>删除文本]
C2 --- D3[旧vnode没有文本]
D2 --- E[新vnode的<br/>children替换 到旧vnode]
D3 --- E[新vnode的<br/>children替换到旧vnode]
B2 --- C3[新vnode没有children,<br/>旧vnode有children]
C3 --- D4[删除旧的vnode的children]
B2 --- C4[旧vnode只有文本]
C4 --- D5[删除旧vnode中内容]
B2 --- C5[两个都是文本<br/>节点,直接替换]
A[patchVnode]--- B3[两者都是文本<br/>节点,直接替换]
下面看下updateChildren的方法
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 首先定义新旧vnode上的起始位置
    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) {
      // oldStartVnode不存在
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      // oldEndVnode不存在
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      // 新旧开始相同(key和标签相等)
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 递归执行patchvnode,start指针向后移动,直到和end相同,结束循环。
        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 {
        //获取新newStartVnode中key对应旧children中有没有某个节点对应的当前的key,
        //没有的话创建element;有的话,判断两个vnode是否相等,相等执行patchvnode更新、不相同直接创建新元素
        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值的比较,如果旧vnode中有对应key的节点,判断两个vnode是否相等,相等执行patchvnode更新、不相同直接创建新元素。如果没有元素直接创建。最后key也不存在,会依次遍历看旧的vnode和新的vnode长度对比,进行插入活删除操作

如果不相同,直接创建新的dom元素。插入到对应的节点,删除oldnode

createElm(
  vnode,
  insertedVnodeQueue,
  // extremely rare edge case: do not insert if old element is in a
  // leaving transition. Only happens when combining transition +
  // keep-alive + HOCs. (#4590)
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
// 操作老的dom
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}

vue从数据变化到视图的更新都发生了什么

首先数据的变化会通过objectdefine.prototype属性拦截对应数据的变化。订阅者会根据对应的数据的变化更新vdom,在oldvdom和vdom之间做diff算法,从而更新视图的变化。

那nexttick的作用呢?

vue中真实的dom的更新是异步的,数据发生改变,vue就会开启一个事件队列,同一个组件的watcher只会被放在事件队列一次,从而减少不必要的更新和dom操作。在下一个tick中执行对应的事件。同样的nextick中的回调是被放在事件队列中的,当完成dom的更新之后,就会去执行事件队列中的会掉方法