当我们改变一个节点的时候,我们其实主要改了以下部分:
那么 diff 算法可以抽象为两部分:
举个栗子:
给定旧数组: [a,b,c,d],新数组: [e,f,g,h],找出新旧数组之间的差异。
我们约定以下名词 - 旧首(旧数组的第一个元素) - 旧尾(旧数组的最后一个元素) - 新首(新数组的第一个元素) - 新尾(新数组的最后一个元素)
一些工具函数:
sameVnode
用于判断节点是否应该复用,这里做了一些简化,实际的diff算法复杂些,这里只用tag 和 key 相同,我们就复用节点,执行patchVnode,即对节点进行修改。
function sameVnode(a, b) { return a.key === b.key && a.tag === b.tag }
createKeyToOldIdx
建立key-index的索引,主要是替代遍历,提升性能
function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map }
旧首 和 新首 对比
if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode.elm, oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; }
旧尾 和 新尾 对比
if (sameVnode(oldEndVnode, newEndVnode)) { //旧尾 和 新尾相同 patchVnode(oldEndVnode.elm, oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; }
旧首 和 新尾 对比
if (sameVnode(oldStartVnode, newEndVnode)) { //旧首 和 新尾相同,将旧首移动到 最后面 patchVnode(oldStartVnode.elm, oldStartVnode, newEndVnode); nodeOps.insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling) oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; }
旧尾 和 新首 对比,将 旧尾 移动到 最前面
if (sameVnode(oldEndVnode, newStartVnode)) { //旧尾 和 新首相同 ,将 旧尾 移动到 最前面 patchVnode(oldEndVnode.elm, oldEndVnode, newStartVnode); nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; }
首尾对比都不符合 sameVnode 的话,尝试用newCh的第一项在 oldCh 内寻找sameVnode,如果在 oldCh 不存在对应的 sameVnode ,则直接创建一个,存在的话则判断符合 sameVnode,则移动 oldCh 对应的节点,不符合 sameVnode ,创建新节点,最后 通过 oldStartIdx > oldEndIdx ,来判断 oldCh 和 newCh 哪一个先遍历完成
oldCh 先遍历完成,则证明 newCh 还有多余节点,需要新增这些节点;newCh 先遍历完成,则证明 oldCh 还有多余节点,需要删除这些节点。
总结:
1. diff 算法的核心是子节点数组对比,思路是通过首尾两端对比。
2. key的作用是:决定节点是否可以复用,建立key-index的索引,主要是替代遍历,提升性能。