知识体系:Vue diff算法的探索

今天特意看了看Vue,在数据发生变化之后,是如何处理新旧节点的,也就是diff算法是如何优化我们重新渲染的,因为创建新的节点相对来说是很消耗性能的,已经创建过的节点是没有必要再创建一遍的,那我还是用debugger的方式学习

以下是我的App.vue 功能很简单就是数据的替换

<template>
  <div id="apps">
    <div v-for="item in tableData" :key="item.id">
      <div>{{item.id}}:{{item.name}} </div>
    </div>
    <div @click="addMember">添加成员</div>
  </div>
</template>

<script>

export default {
  data:function(){
    return {
      tableData:[
        {
          name:"yjt",
          id:1
        },
        {
          name:"wzh",
          id:2
        },
        {
          name:"gjf",
          id:3
        },
        {
          name:"zjz",
          id:4,
        }
      ]
    }
  },
  beforeCreate(){
    console.log("app beforeCreate")
  },
  created(){
    console.log("app created")
  },  
  mounted(){
    console.log("app mounted")
  },
  methods:{
    addMember(){
      this.tableData = [{
        name:'znf',
        id:5
      },
      {
        name:'czk',
        id:6
      },
      {
        name:'ztx',
        id:7
      }]
    }
  }
}
</script>

当我们点击添加成员按钮,就会触发页面的重新渲染,先深度遍历递归的生成新的虚拟节点Vnode,然后再创建真实的dom
因为我们就想了解一下diff, 我就直接进入关键代码,就是这个patchVnode函数
这个函数的参数oldVnode 就是我们初始化创建的虚拟节点Vnode
vnode就是我们新生成的虚拟Vnode
下面是源代码:

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {

	。。。省略
    var elm = vnode.elm = oldVnode.elm;

   
	。。。省略
    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)) {
      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)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }  //diff算法进行新旧节点的比对
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch);
        }
        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) {
      nodeOps.setTextContent(elm, vnode.text);
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
    }
  }

这里我们的新旧节点都是有孩子节点的,所以我们的逻辑就会进入
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
我们的diff算法就是在updateChildren函数中
源码:

 function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    var oldStartIdx = 0;
    var newStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
    var 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);
    }
  }

不是特别长,这个函数首先就是先赋值,找到旧节点的开始索引,结束索引,开始Vnode, 结束Vnode以及新节点的开始索引,结束索引,开始Vnode, 结束Vnode。
然后开始循环每一个Vnode进行判断,依次判断的顺序是:
1.旧Vnode是否存在
2.新Vnode是否存在
3.旧的开始Vnode和新的开始Vnode是否是相同的节点
4.旧的结束Vnode和新的结束Vnode是否是相同的节点
5.旧的开始Vnode和新的结束Vnode是否是相同的节点
6.旧的结束Vnode和新的开始Vnode是否是相同的节点
7.如果上面都不满足,就会判断当前的Vnode是否是已经创建过的节点,如果是那么就直接将现有的DoM插入,如果是新的节点就会进行创建新元素,然后插入到父节点中去

两个节点相同的依据就是先判断这个key是否相同,然后就是标签,属性是否定义
这是判断两个节点是否相同的源码

	function sameVnode (a, b) {
	  return (
	    a.key === b.key && (
	      (
	        a.tag === b.tag &&
	        a.isComment === b.isComment &&
	        isDef(a.data) === isDef(b.data) &&
	          (a, b)
	      ) || (
	        isTrue(a.isAsyncPlaceholder) &&
	        a.asyncFactory === b.asyncFactory &&
	        isUndef(b.asyncFactory.error)
	      )
	    )
	  )
	}

而我们的例子中,很明显新旧数据的key都是不一样的,但是我们的结束节点是一样的,都是这个添加成员这个按钮,
所以我们会进入第四种情况,旧结束Vnode和新结束Vnode相同的逻辑,执行的函数是
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
这个不就是我们刚刚的函数么,这里不就明白了,diff算法是深度遍历的递归比对子节点,直到没有子节点为止
结束第一个循环,其他的几次循环都是不满足以上六种情况的,而是进入第七种情况

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];

先通过createKeyToOldIdx创建index和key的映射,将旧节点变成一个映射表
然后查找新的Vnode的key在这个映射中是否存在,我们旧Vnode的key为[1,2,3,4],而我们新的Vnode是5,6,7
很明显是不存在的,那么我们就会走
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
对,就是直接创建新的节点,并直接插入到父元素中去
在这里插入图片描述
当循环结束,这个时候我们的页面应该是这样的,新的元素都通过创建新元素的形式插入到我们的页面中来了
这样我们新旧Vnode的比对就完成了,那么这些旧节点就要处理一下,此时newStartIdx > newEndId条件满足,最后我们就会进入这个逻辑

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);
}

removeVnodes(oldCh, oldStartIdx, oldEndIdx);这个函数,看名字我们也能知道这个是移除节点,参数是旧的Vnode,开始索引0,结束索引3,而我们旧的Vnode的0到3的索引不就正好是key为1,2,3,4的这四个DOM么,最后我们的页面就变成了。
在这里插入图片描述
这次diff,我们的添加成员这个dom是延用旧的DOM,而我们key为5,6,7的节点则是我们新创建插入的.
这就是本次调试的完整流程,总结一下

diff算法用通俗的话讲就是,深度遍历递归的进行新旧节点的对比,先是判断是否符合上面的六种情况
1.旧Vnode是否存在
2.新Vnode是否存在
3.旧的开始Vnode和新的开始Vnode是否是相同的节点
4.旧的结束Vnode和新的结束Vnode是否是相同的节点
5.旧的开始Vnode和新的结束Vnode是否是相同的节点
6.旧的结束Vnode和新的开始Vnode是否是相同的节点
如果都不符合再创建映射表,通过映射表查找之后的元素是否是创建过的,如果是已经创建过的Dom那么就直接使用,如果是没有创建过的节点,那么就会创建元素再插入,这样就减少了DOM创建的次数,达到了性能提升的目的。

我这个只是一种情况,其他的情况还是要我们自己写例子,自己实践,一步步debugger, 实践才是学习最好的方法
这就是我今天的分享,谢谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值