Open AnnVoV opened 6 years ago
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); } } }
// 比对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) } }
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, 那这个好处到底在于哪里呢?
:key='xx'
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
patch 方法骨架
patchVnode 方法
updateChildren 方法
看一下大的结构,分为以下几个维度比较:
比较oldStartVnode 与 newStartVnode
比较oldEndVnode 与 newEndVnode
比较oldStartVnode 与 newEndVnode
比较oldEndVnode与newStartVnode
上面4中情况都不符合时,单独讲
依次判断他们是否为sameVnode,如果是,则再进入patchVnode方法
下面我们结合实际的例子,假设我们的模板是下面的样子, 原先data是:
前面的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呢?让我们大致看下这个方法
我们来看下createKeyToOldIdx 方法, 大致从这个方法可以看出和
:key='xx'
这种相关,这个一般在li中vue会给我们建议设置key, 那这个好处到底在于哪里呢?举个例子,如果我们不绑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就好了
其实也就是这张经典的图
参考资料
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