Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅析Vue 中的patch和diff(下) #263

Open
AnnVoV opened this issue Apr 28, 2018 · 0 comments
Open

浅析Vue 中的patch和diff(下) #263

AnnVoV opened this issue Apr 28, 2018 · 0 comments

Comments

@AnnVoV
Copy link

AnnVoV commented Apr 28, 2018

patch 方法骨架

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    ...
    if(isUndef(oldVnode)) {
       // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    }else{
       // 我们上一次的oldVnode 是Virtual DOM 所以isRealElement为false 
       var isRealElement = isDef(oldVnode.nodeType);
       if(!isRealElement && sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
       }
    }
}

patchVnode 方法

// 比对oldVnode 与 vnode 的方法
function patchVnode(oldVnode, vnode, ...) {
	if (oldVnode === vnode) {
      return
    }
    // elm 中存储的是真实的dom结构,把旧的dom结果先赋值给新vnode
    var elm = vnode.elm = oldVnode.elm;
  
   ...

    // 如果vnode 节点不是text节点
    if(isUndef(vnode.text)) {
        if(isDef(oldCh) && isDef(ch)) {
			if(oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)            
        }else if(isDef(ch)) {
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        }else if(isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }else if(isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '');
		}
    }else if(oldVnode.text !== vnode.text) {
         // 如果text内容不一样,直接更新
        nodeOps.setTextContent(elm, vnode.text)
	}
}
  • 当oldVnode 与 vnode 相同时,说明不需要比对直接return
  • 先看下面的else if, 如果vnode节点是text节点,直接通过setTextContent方法修改节点文本内容
  • 如果新的Vnode 是非文本节点,需要分下面几种情况
    • 如果新老节点都有children且children不一样,则updateChildren(这个方法后面细说)
    • 如果新children有定义,旧的children未定义, 则基于旧dom来addVnode
    • 如果新children未定义,旧children有定义,则remove掉旧的dom的chidren
    • 如果旧的dom是个text类型,则清空旧dom 文本

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;

    // 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);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
		// 注意这里涉及到节点移动
        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);
        // 注意这里涉及到节点移动  
        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);
            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(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

看一下大的结构,分为以下几个维度比较:

  • 比较oldStartVnode 与 newStartVnode

  • 比较oldEndVnode 与 newEndVnode

  • 比较oldStartVnode 与 newEndVnode

  • 比较oldEndVnode与newStartVnode

  • 上面4中情况都不符合时,单独讲

    依次判断他们是否为sameVnode,如果是,则再进入patchVnode方法

    function sameVnode (a, b) {
        console.log(a);
      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)
          )
        )
      )
    }

    下面我们结合实际的例子,假设我们的模板是下面的样子, 原先data是:

    // 原先
    data = {a:1, b:1}
    // 之后
    data = {a: 2, b:3}
    <section>
        <div v-if="a==1">
            <div>测试1</div>
            <p>内容2</p>
            <div>内容3</div>
            <p>内容4</p>
        </div>
        <div v-if="a==2">
            <p>内容4</p>
            <p>内容2</p>
            <div>内容3</div>
        </div>
    </section>

​ 前面的4种情况挺容易让人理解的,就是从根节点开始进入patchVnode(oldVnode, vnode),若根节点有children进入updateChildren方法,updateChildren里面定义了新老vnode 树的索引: oldStartIdx, oldEndIdx, newStartIdx, newEndIdx。然后进行4种维度的两两对比。当oldStart与newEnd一致时,会更新oldStart 同时将这个节点移动到oldEnd后面位置; 同理当oldEnd与newStart一致时也会更新并对节点进行移动;如果oldStart 与 newStart一致,直接更新节点内容;如果oldEnd与newEnd一致同理直接更新节点内容。

​ 如果这4种情况都不满足,怎么处理?会进入下面的阶段, 下面这个阶段大部分会进入createElm 这个方法,那什么时候会进入createKeyToOldIdx呢?让我们大致看下这个方法

} 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);
            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 方法, 大致从这个方法可以看出和:key='xx'这种相关,这个一般在li中vue会给我们建议设置key, 那这个好处到底在于哪里呢?

function createKeyToOldIdx (children, beginIdx, endIdx) {
  var i, key;
  var map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) { map[key] = i; }
  }
  return map
}

举个例子,如果我们不绑key, 如果我们遍历items=[1,2,3,4,5]; 后面更新数据为[1,2,6,3,4,5] 那么dom更新的过程,按照上面的分析必然为下图所示:

而当我们设置了key时,因为进入sameVnode判断的时候会判断key, 所以我们的比较会变成,下图所示,1,2都是同级比较,然后到3的时候会满足oldEnd与newEnd一致,所以开始进入5的比较,依次类推,所以dom都被复用了,最后只要在对应位置插入6就好了

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的作用主要是为了高效的更新虚拟DOM。

参考资料

1.vue2.0 virtual-dom实现简析 DDFE/DDFE-blog#18
2.vue2.0中 v-for的key 到底有什么用? https://www.zhihu.com/question/61064119
3.VirtualDOM与diff(Vue实现) https://github.com/answershuto/learnVue/blob/master/docs/VirtualDOM%E4%B8%8Ediff(Vue%E5%AE%9E%E7%8E%B0).MarkDown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant