浅析Vue 中的patch和diff(下)
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实现简析 https://github.com/DDFE/DDFE-blog/issues/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