Vue 数据渲染中最核心的的部分就是 diff算法 的应用,本文从源码入手,结合实例,一步步解析 diff 算法的整个流程。vue
diff算法是一种经过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,因此时间复杂度只有 O(n)。diff算法的在不少场景下都有应用,例如在 vue 虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较更新时,就用到了该算法。diff算法有两个比较显著的特色:node
一、比较只会在同层级进行, 不会跨层级比较。git
二、在diff比较的过程当中,循环从两边向中间收拢github
本着对 diff 过程的认识和 vue 源码的学习,咱们经过 vue 源码的解读和实例分析来理清楚 diff 算法的整个流程,下面把整个 diff 流程拆成三步来具体分析:算法
vue 的虚拟 dom 渲染真实 dom 的过程当中首先会对新老 VNode 的开始和结束位置进行标记:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。c#
let oldStartIdx = 0 // 旧节点开始下标 let newStartIdx = 0 // 新节点开始下标 let oldEndIdx = oldCh.length - 1 // 旧节点结束下标 let oldStartVnode = oldCh[0] // 旧节点开始vnode let oldEndVnode = oldCh[oldEndIdx] // 旧节点结束vnode let newEndIdx = newCh.length - 1 // 新节点结束下标 let newStartVnode = newCh[0] // 新节点开始vnode let newEndVnode = newCh[newEndIdx] // 新节点结束vnode
通过第一步以后,咱们初始的新旧 VNode 节点以下图所示:dom
标记好节点位置以后,就开始进入到的 while 循环处理中,这里是 diff 算法的核心流程,分状况进行了新老节点的比较并移动对应的 VNode 节点。while 循环的退出条件是直到老节点或者新节点的开始位置大于结束位置。源码分析
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { ....//处理逻辑 }
接下来具体介绍 while 循环中的处理逻辑, 循环过程当中首先对新老 VNode 节点的头尾进行比较,寻找相同节点,若是有相同节点知足 sameVnode(能够复用的相同节点) 则直接进行 patchVnode (该方法进行节点复用处理),而且根据具体情形,移动新老节点的 VNode 索引,以便进入下一次循环处理,一共有 2 * 2 = 4 种情形。下面根据代码展开分析:学习
if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }
else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) 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, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }
若是都不知足以上四种情形,那说明没有相同的节点能够复用,因而则经过查找事先创建好的以旧的 VNode 为 key 值,对应 index 序列为 value 值的哈希表。从这个哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,若是二者知足 sameVnode 的条件,在进行 patchVnode 的同时会将这个真实 dom 移动到 oldStartVnode 对应的真实 dom 的前面;若是没有找到,则说明当前索引下的新的 VNode 节点在旧的 VNode 队列中不存在,没法进行节点的复用,那么就只能调用 createElm 建立一个新的 dom 节点放到当前 newStartIdx 的位置。spa
else {// 没有找到相同的能够复用的节点,则新建节点处理 /* 生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值作铺垫) 好比childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 结果生成{key0: 0, key1: 1, key2: 2} */ if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) /*若是newStartVnode新的VNode节点存在key而且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element /*newStartVnode没有key或者是该key没有在老节点中找到则建立一个新的节点*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { /*获取同key的老节点*/ vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { /*若是新VNode与获得的有相同key的节点是同一个VNode则进行patchVnode*/ patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //由于已经patchVnode进去了,因此将这个老节点赋值undefined oldCh[idxInOld] = undefined /*当有标识位canMove实能够直接插入oldStartVnode对应的真实Dom节点前面*/ canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element /*当新的VNode与找到的一样key的VNode不是sameVNode的时候(好比说tag不同或者是有不同type的input标签),建立一个新的节点*/ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] }
再来看咱们的实例,第一次循环后,找到了旧节点的末尾和新节点的开头(都是D)相同,因而直接复用 D 节点做为 diff 后建立的第一个真实节点。同时旧节点的 endIndex 移动到了 C,新节点的 startIndex 移动到了 C。
紧接着开始第二次循环,第二次循环后,一样是旧节点的末尾和新节点的开头(都是C)相同,同理,diff 后建立了 C 的真实节点插入到第一次建立的 B 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E。
接下来第三次循环中,发现 patchVnode 的4种情形都不符合,因而在旧节点队列中查找当前的新节点 E,结果发现没有找到,这时候只能直接建立新的真实节点 E,插入到第二次建立的 C 节点以后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动。
第四次循环中,发现了新旧节点的开头(都是A)相同,因而 diff 后建立了 A 的真实节点,插入到前一次建立的 E 节点后面。同时旧节点的 startIndex 移动到了B,新节点的startIndex 移动到了B。
第五次循环中,情形同第四次循环同样,所以 diff 后建立了 B 真实节点 插入到前一次建立的 A 节点后面。同时旧节点的 startIndex 移动到了C,新节点的 startIndex 移动到了F。
这时候发现新节点的 startIndex 已经大于 endIndex 了。再也不知足循环的条件了。所以结束循环,接下来走后面的逻辑。
当 while 循环结束后,根据新老节点的数目不一样,作相应的节点添加或者删除。若新节点数目大于老节点则须要把多出来的节点建立出来加入到真实 dom 中,反之若老节点数目大于新节点则须要把多出来的老节点从真实 dom 中删除。至此整个 diff 过程就已经所有完成了。
if (oldStartIdx > oldEndIdx) { /*所有比较完成之后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多, 因此这时候多出来的新节点须要一个一个建立出来加入到真实Dom中*/ refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) //建立 newStartIdx - newEndIdx 之间的全部节点 } else if (newStartIdx > newEndIdx) { /*若是所有比较完成之后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多于新节点,这个时候须要将多余的老节点从真实Dom中移除*/ removeVnodes(oldCh, oldStartIdx, oldEndIdx) //移除 oldStartIdx - oldEndIdx 之间的全部节点 }
再回过头看咱们的实例,新节点的数目大于旧节点,须要建立 newStartIdx 和 newEndIdx 之间的全部节点。在咱们的实例中就是节点 F,所以直接建立 F 节点对应的真实节点放到 B 节点后面便可。
经过上述的源码和实例的分析,咱们完成了 Vue 中 diff 算法的完整解读。若是想要了解更多的 Vue 源码。欢迎进入咱们的github进行查看,里面有Vue源码分析另外几篇文章,另外对 Vue 工程的每一行源码都作了注释,方便你们的理解。~~~~