vue2手撕源码-diff算法

上文可知,render 函数会生成深层次的虚拟 dom, 若是页面第一次渲染,直接插入到真实dom 中,如果不是第一次,那么就要进行新老 dom树对比。

首先思考三个问题:

对比原则是什么?

同一层之间进行对比,不能跨层级, 比如:

// 旧的:
<div>
    <div>
        <p></p>
    </div>
</div>

// 新的: 
<div>
    <div></div>
    <p></p>
</div>
复制代码

新老dom区别就是p标签的位置,看起老可以直接把p标签拿出来,直接复用就可以了,但是真正对比的时候却不是这样的,而是删除旧的里面的p标签,插入新的p标签,原因就是diff算法的对比逻辑是在同一层级展开的。

怎么判断节点是否相同可以复用? 

源码里通过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)
      )
    )
  )
}
复制代码

这里暂且理解为,如果存在循环且有key值得话,比较key和tagName是否相同,如果是普通节点不存在key值得话,比较tagName是否相同。

vnode格式是怎样的呢? 

// body下的 <div id="v" class="classA"><div> 对应的 oldVnode 就是 

{ 
	el:  div  //对真实的节点的引用,本例中就是document.querySelector('#id.classA')  
  tagName: 'DIV',   //节点的标签  
  sel: 'div#v.classA'  //节点的选择器  
  data: null,       // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style  
  children: [], //存储子节点的数组,每个子节点也是vnode结构  
  text: null,    //如果是文本节点,对应文本节点的textContent,否则为null
}
复制代码

接下来继续上文,分析patch过程:

patch过程

vue源码中是这么做的:

image.png

我们可以看到,patch方法中对比了 oldVnode 和vnode是否相似,相似就进行进一步对比,执行patchVnode方法,不相似进行了一大堆操作。。。

我们将patch代码简化为:

function patch(oldVnode, vnode) {
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
    } else {
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl) // 获取oldVnodede 父节点
        createEle(vnode)  // 创建vnode,这里是dom操作
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将vnode插入到oldVnode的后面
            api.removeChild(parentEle, oldVnode.el)  // 删除oldVnode
            oldVnode = null
        }
    }
    return vnode
}
复制代码

总结一下:

  1. patch方法中对比了 oldVnode 和vnode是否相似,相似就进行进一步对比,执行patchVnode方法
  2. 不相似,获取oldVnodede 父节点,创建vnode,这里是dom操作
  3. parentEle父节点不为空,将vnode插入到父节点下面的oldVnode的后面
  4. 删除oldVnode

patchVnode 节点对比过程

function patchVnode (oldVnode, vnode) {
	  var oldCh = oldVnode.children;
    var ch = vnode.children;
    console.log(90, vnode);
    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)) { // 新老节点都有children,则比较children, updateChildren(diff算法)
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
      } else if (isDef(ch)) {  // 新节点有子节点,老节点没有子节点,老节点就新增
        {
          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)) {  //新、老节点都没children,并且老节点有text,则删除老节点text
        nodeOps.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text);  // 新节点有文本,新老节点文本不一样,替换文本
    }
}
复制代码

节点对比:

  1. 新节点有文本,新老节点文本不一样,替换文本
  2. 否则,新老节点都有children,则比较children, updateChildren(diff算法)
  3. 否则,新节点有子节点,老节点没有子节点,老节点就新增
  4. 否则,新节点没有子节点,老节点有子节点,老节点就删除
  5. 否则,新、老节点都没children,并且老节点有text,则删除老节点text

diff算法

diff算法用于对比新老节点的children

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;

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

  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);
  }
}
复制代码

总结一下过程:

  1. 前前(old, new): 前前节点相同,则进行节点对比patchVnode, 前前index + 1
  1. 后后(old, new): 后后节点相同,则进行节点对比patchVnode, 后后index - 1
  1. 前后(old, new): 前后相同,则将旧节点开始节点移动到旧节点结束节点的后面,前面旧节点index + 1, 后面老节点index - 1
  1. 后前(old, new): 后前相同,将旧节点结束节点移动到旧节点开始节点的前面,后面旧节点index -1, 前面新节点index + 1
  1. 当以上情况都不符合时,生成一个key与旧vnode对应的哈希表,最后生成的对象就是以children的key为属性,递增的数字为属性值的对象

    • 如果有key,则在哈希表里面找到与新的开始节点相同的旧的里面对应key相同的旧的index,没有key则遍历oldVnode找出相似的节点对应的index
    • 如果没找到相似的,则 createElm 创建一个新的节点插入到 parentElm 中;
    • 如果找到相似的 oldVnode,则 patchNode 后,再在真实 dom 中将该 oldVnode 移动到oldStartVnode.elm前
  2. oldStartIdx > oldEndIdx,说明 oldNode 到头了,但是 newNode 中还有多余的,那么就把newNode中多余的全部都addVnodes

  1. 如果 newStartIdx > newEndIdx,说明了 newNode 到头了,但是 oldVnode 中还有多余的,因此 removeVnodes

猜你喜欢

转载自juejin.im/post/7031460725973319687