vue-diff算法

虚拟DOM(Virtual Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的,避免频繁对真实DOM操作造成布局重排、回流重绘等性能开销。

1、diff算法:

渲染真实DOM的开销是很大的,轻微的操作都可能导致页面重新排版,非常耗性能。 相对于DOM对象,js对象处理起来更快,而且更简单。 通过diff算法对比新旧vdom之间的差异,可以批量的、最小化的执行 dom操作,从而提高性能。

2、diff时间复杂度分析:

常规:O(n^3) 遍历树1; 遍历树2; 排序; 1000个节点,十亿的量级。

1.png

vue diff:O(n) 只比较同一层级 ;tag不相同,直接删掉重建; 通过key来标识区分相同节点。

2.png

3、vue diff key优化特点

<div id="demo">
    <p v-for="item in arr" :key="item">
        {{a}}
    </p>
</div>
<script>
const app = new Vue({
    el: '#demo',
    data: { arr: ['A', 'B', 'C', 'D'] },
    mounted() {
        setTimeout(() => {
            this.arr.splice(1, 0, 'E');
        }, 1000);
    }
});
</script>

旧节点:A、B、C、D
新节点:A、E、B、C、D

节点没设置key:如下,将会对4个节点做更新操作
nokey.gif
节点设置了key:如下,只对1个节点做更新操作
key.gif

diff算法核心原理:

diff.png
1、sameVnode:判断两个节点是否为同一个节点
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
判断条件:

1.1 两个节点key是否相同(两个key都没有,即 undefined === undefined)

1.2 两个节点tag标签名是否一样

1.3 两个节点是否都为注释节点

1.4 两个节点的data isDef是否都相等(isDef:data !== undefined && data !== null)

1.5 两个节点的input类型是否相同

1.6 节点a是否为异步占位

1.7 两个节点的异步函数是否相等

1.8节点b异步函数的error是否为空(isUndef:data === undefined && data === null)

2、patchVnode:更新节点
function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 新旧node相同,return   
  if (oldVnode === vnode) {
    return
  }
 
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode);
  }
 
  var 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
  }
 
  var i;
  var data = vnode.data;
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode);
  }
 
  var oldCh = oldVnode.children;
  var ch = vnode.children;
  if (isDef(data) && isPatchable(vnode)) {
    // 调用update回调以及update钩子
    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); }
  }
  if (isUndef(vnode.text)) {
    // 新节点text文本属性不存在
    if (isDef(oldCh) && isDef(ch)) {
      // 新旧节点都有子节点,调用updateChildren重排子节点
      if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
    } else if (isDef(ch)) {
      // 只有新节点有子节点,调用addVnodes添加子节点
      {
        checkDuplicateKeys(ch);
      }
      if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // 只有旧节点有子节点,调用removeVnodes添加子节点
      removeVnodes(oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      // 只有旧节点有文本,调用setTextContent清空文本
      nodeOps.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // 新旧节为文本节点并且文本不一致,调用setTextContent清空文本
    nodeOps.setTextContent(elm, vnode.text);
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
  }
}
更新条件:

2.1 新旧节点都有子节点,调用updateChildren重排子节点

2.2 只有新节点有子节点,调用addVnodes添加子节点

2.3 只有旧节点有子节点,调用removeVnodes移除子节点

2.4 如果是文本节点,调用setTextContent更新节点文本内容

3、updateChildren:更新子节点
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  var oldStartIdx = 0; // 旧子节点start下标(游标)
  var newStartIdx = 0; // 新子节点start下标(游标)
  var oldEndIdx = oldCh.length - 1; // 旧子节点end下标(游标)
  var newEndIdx = newCh.length - 1; // 新子节点end下标(游标)
  var oldStartVnode = oldCh[0]; // 旧start节点(旧头)
  var newStartVnode = newCh[0]; // 新start节点(新头)
  var oldEndVnode = oldCh[oldEndIdx]; // 旧end节点(旧尾)
  var newEndVnode = newCh[newEndIdx]; // 新end节点(新尾)
  var 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
  var canMove = !removeOnly;
 
  {
    checkDuplicateKeys(newCh);
  }
 
  // oldStartIdx大于oldEndIdx,或者newStartIdx大于newEndIdx时候跳出循环
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // 旧头不存在,oldStartIdx++
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
 
    } else if (isUndef(oldEndVnode)) {
      // 旧尾不存在,oldEndIdx--
      oldEndVnode = oldCh[--oldEndIdx];
 
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 旧头、新头相同,更新节点,oldStartVnode++、newStartVnode++
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
 
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 旧尾、新尾相同,更新节点,oldEndIdx--、newEndIdx--
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
 
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 旧头、新尾相同,更新节点,并且将旧头移到最后,oldStartIdx++、newEndIdx--
      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
      // 旧尾、新头相同,更新节点,并且将旧尾移到最前面,newStartIdx++、oldEndIdx--
      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 {
          // 节点不同,新建节点
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
 
  if (oldStartIdx > oldEndIdx) {
    // 旧子节点先遍历完,新子节点还有剩,调用addVnodes添加节点
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  } else if (newStartIdx > newEndIdx) {
    // 新子节点先遍历完,旧子节点还有剩,调用removeVnodes添加节点
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}
3.1 新旧子节点未遍历完毕

3.1.1 旧头不存在,将旧头游标往后移一位

3.1.2 旧尾不存在,将旧尾游标往前移一位

3.1.3 旧头、新头相同,更新节点,并将头部游标往后移一位

3.1.4 旧尾、新尾相同,更新节点,并将尾部游标往前移一位

3.1.5 旧头、新尾相同,更新节点,并且将旧头移到尾部,旧头游标往后移一位,新尾游标往前移一位

3.1.6 旧尾、新头相同,更新节点,并且将旧尾移到头部,新头游标往后移一位,旧尾游标往前移一位

3.1.7 拿新头遍历旧子节点,找不到则新建一个节点;找到判断节点是否相同,相同则更新节点,移动老节点,不同则新建一个节点

3.2 新旧子节点遍历完毕

3.2.1 旧子节点先遍历完毕,说明有新增节点,批量增加

3.2.2 新子节点先遍历完毕,说明有节点删除,批量移除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值