>[success] # patch 方法分析(三) ~~~ 1.上一个章节分析后,patch更新主要分为两块,一块是新老虚拟dom相同,一块是不同,这里是对 相同位置的分析 ~~~ >[info] ## 分析patch 里面patchVnode ~~~ 1.在patch 里如果 oldVnode 和 vnode 相同(key 和 sel 相同),调用 patchVnode(),找节点的差异并更新 DOM 2.在这里主要对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM 3.分析执行过程(分析过程直接看虚拟dom比较的代码,不分析钩子部分): 3.1.如果 vnode.text 未定义 3.1.1.如果 oldVnode.children 和 vnode.children 都有值 3.1.1.1.调用 updateChildren() 3.1.1.2.使用 diff 算法对比子节点,更新子节点 3.1.2.如果 vnode.children 有值, oldVnode.children 无值 3.1.2.1.清空 DOM 元素 3.1.2.2.调用 addVnodes() ,批量添加子节点 3.1.3.如果 oldVnode.children 有值, vnode.children 无值 3.1.3.1.调用 removeVnodes() ,批量移除子节点 3.1.4.如果 oldVnode.text 有值 3.1.4.1.清空 DOM 元素的内容 3.2.如果设置了 vnode.text 并且和和 oldVnode.text 不等 3.2.1.如果老节点有子节点,全部移除 3.2.2.设置 DOM 元素的 textContent 为 vnode.text ~~~ ~~~ // 如果 vnode.text 未定义,有text 就没有children if (isUndef(vnode.text)) { // 如果新老节点都有 children if (isDef(oldCh) && isDef(ch)) { // 使用 diff 算法对比子节点,更新子节点 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { // 如果新节点有 children,老节点没有 children // 如果老节点有text,清空dom 元素的内容 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); // 批量添加子节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 如果老节点有children,新节点没有children // 批量移除子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { // 如果老节点有 text,清空 DOM 元素 api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { // 如果没有设置 vnode.text if (isDef(oldCh)) { // 如果老节点有 children,移除 removeVnodes(elm, oldCh, 0, oldCh.length - 1); } // 设置 DOM 元素的 textContent 为 vnode.text api.setTextContent(elm, vnode.text!); ~~~ >[info] ## updateChildren -- diff ~~~ 1.'patchVnode'是对具有相同父节点(这里判断依据是key和sel选择器相同则认为是相同父节点), 新老虚拟dom此时要比也就是自己的子节点是否相同,其他情况上面分析过,其中最复杂的情况就是 新老子节点都是有children 情况,最笨的方法拿每一个老节点虚拟dom和新节点的去比较,如下图嵌套三层 的情况下时间复杂度为 O(n^3) 2.如何优化? 2.1.需要先假设一个前提,'在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点' 2.2.此时我们要做的就是同级比较即可,每一层和每一层去比较,如果依旧采用的思路是拿每一个老节点 虚拟dom和新节点的去比较,虽然变成了层和层的去比较此时下图的时间复杂度为 每段O(n^2)比较和,时间上 感觉比之前快了 2.3.现在再换一个思路利用指针进行找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的 时间复 杂度为 O(n) 2.4.计算上的问题解决了,考虑的就是dom复用,有时候重新排序只是新老dom节点位置发生变化,如果能复用 相同的dom而不是重新创建dom这样就又可以减少一部分性能开销 ~~~ ![](https://img.kancloud.cn/df/75/df757beff86ea4bdc8e2997d5225b323_599x280.png) >[danger] ##### 情况分析 ~~~ 1.oldStartVnode / newStartVnode (旧开始节点 / 新开始节点) 2.oldEndVnode / newEndVnode (旧结束节点 / 新结束节点) 3.oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点) 4.oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 5.前四种比较都不同,则使用新开始节点的key值在【旧开始节点-旧结束节点】的子数组中找相同key的节点。 若没有在子数组中找到相同key节点则新开始节点是新节点,创建一个该节点的真实DOM节点插入到旧开始节 点指向的真实DOM节点之前;若找到相同key的节点获取该旧节点,比较新、旧节点的选择器是否相同, 选择器不同则创建一个该节点的真实DOM节点插入到旧开始节点指向的真实DOM节点之前,选择器相同则 对比两个节点的差异执行相应的操作(如更新DOM节点),并将子数组中key节点指向到真实DOM节点移动 到到旧开始索引指向的真实DOM节点之前(移动的是真实DOM而不是虚拟DOM,虚拟DOM无变化)。 最后将新开始索引指向下一个节点 ~~~ [参考视频动画链接](https://www.bilibili.com/video/BV1b5411V7i3?t=479) ![](https://img.kancloud.cn/c8/ac/c8ac0f422e27222abd614252e184c2f6_2600x4182.png) >[danger] ##### 代码实现部分 ~~~ function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx: KeyToIndexMap | undefined let idxInOld: number let elmToMove: VNode let before: any while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 索引变化后,可能会把节点设置为空 if (oldStartVnode == null) { // 节点为空移动索引 oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left } else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx] } else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx] } else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx] // 比较开始和结束节点的四种情况 } else if (sameVnode(oldStartVnode, newStartVnode)) { // 1. 比较老开始节点和新开始节点 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 2.比较老结束节点和新的结束节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 3.比较老开始节点和新的结束节点 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 4.比较老结束节点和新的开始节点 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 开始节点和结束节点都不相同 // 使用newStartNode 的key在老节点数组中找相同节点 // 先设置记录 key 和index对象 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } // 遍历 newStartVnode,从旧节点中找相同key的oldVnode的索引 idxInOld = oldKeyToIdx[newStartVnode.key as string] // 如果是新的vnode if (isUndef(idxInOld)) { // New element // 如果没找到,newStartNode 是新节点 // 创建元素插入DOM树 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!) } else { // 如果找到相同key的老节点,记录到elmToMove遍历 elmToMove = oldCh[idxInOld] if (elmToMove.sel !== newStartVnode.sel) { // 如果新旧节点的选择器不同 // 创建新开始节点对应的DOM元素,插入到DOM树种 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!) } else { // 如果相同,patchVnode() // 把elmToMove 对应的DOM元素,移动到左边 patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined as any api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!) } } // 重新给newStartVnode 复制,指向下一个新节点 newStartVnode = newCh[++newStartIdx] } } // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成 if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { if (oldStartIdx > oldEndIdx) { // 如果老节点数组先遍历完成,说明有新的节点剩余 // 把剩余的新节点都插入到右边 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else { // 如果新节点数组先遍历完成,说明老节点有剩余 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } } ~~~ >[danger] ##### 虚拟dom中key的好处 ~~~ Key 是用来优化 Diff 算法的。Diff算法核心在于同层次节点比较,Key 就是用于在比较同层次新、旧节点时, 判断其是否相同。 Key 一般用于生成一列同类型节点时使用,这种情况下,当修改这些同类型节点的某个内容、变更位置、删除、 添加等时,此时界面需要更新视图,Vue 会调用 patch 方法通过对比新、旧节点的变化来更新视图。 其从根节点开始若新、旧 VNode 相同,则调用 patchVnode patchVnode 中若新节点没有文本,且新节点和旧节点都有有子节点,则需对子节点进行 Diff 操作,即调用 updateChildren,Key 就在 updateChildren 起了大作用 updateChildren 中会遍历对比上步中的新、旧节点的子节点,并按 Diff 算法通过 sameVnode 来判断要对比 的节点是否相同 若这里的子节点未设置 Key,则此时的每个新、旧子节点在执行 sameVnode 时会判定相同,然后再次执行一次 patchVnode 来对比这些子节点的子节点 若设置了 Key,当执行 sameVnode 若 Key 不同 sameVnode 返回 false,然后执行后续判断; 若 Key 相同 sameVnode 返回 true,然后再执行 patchVnode 来对比这些子节点的子节点 即,使用了 Key 后,可以优化新、旧节点的对比判断,减少了遍历子节点的层次,少使用很多次 patchVnode ~~~