在开始遍历diff前,首先给oldCh和newCh分别分配一个startIndex和endIndex来作为遍历的索引,当oldCh或者newCh遍历完后(遍历完的条件就是oldCh或者newCh的startIndex >= endIndex),就停止oldCh和newCh的diff过程。接下来通过实例来看下整个diff的过程(节点属性中不带key的情况):
首先从第一个节点开始比较,不管是oldCh还是newCh的起始或者终止节点都不存在sameVnode,同时节点属性中是不带key标记的,因此第一轮的diff完后,newCh的startVnode被添加到oldStartVnode的前面,同时newStartIndex前移一位;
第二轮的diff中,满足sameVnode(oldStartVnode, newStartVnode),因此对这2个vnode进行diff,最后将patch打到oldStartVnode上,同时oldStartVnode和newStartIndex都向前移动一位
第三轮的diff中,满足sameVnode(oldEndVnode, newStartVnode),那么首先对oldEndVnode和newStartVnode进行diff,并对oldEndVnode进行patch,并完成oldEndVnode移位的操作,最后newStartIndex前移一位,oldStartVnode后移一位;
第四轮的diff中,过程同步骤3;
第五轮的diff中,同过程1;
遍历的过程结束后,newStartIdx > newEndIdx,说明此时oldCh存在多余的节点,那么最后就需要将这些多余的节点删除。
在vnode不带key的情况下,每一轮的diff过程当中都是起始和结束节点进行比较,直到oldCh或者newCh被遍历完。而当为vnode引入key属性后,在每一轮的diff过程中,当起始和结束节点都没有找到sameVnode时,首先对oldCh中进行key值与索引的映射:
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
createKeyToOldIdx方法,用以将oldCh中的key属性作为键,而对应的节点的索引作为值。然后再判断在newStartVnode的属性中是否有key,且是否在oldKeyToIndx中找到对应的节点。
如果不存在这个key,那么就将这个newStartVnode作为新的节点创建且插入到原有的root的子节点中:
if (isUndef(idxInOld)) { // New element // 创建新的dom节点 // 插入到oldStartVnode.elm前面 // 参见createElm方法 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] }
如果存在这个key,那么就取出oldCh中的存在这个key的vnode,然后再进行diff的过程:
elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !elmToMove) { // 将找到的key一致的oldVnode再和newStartVnode进行diff if (sameVnode(elmToMove, newStartVnode)) { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) // 清空这个节点 oldCh[idxInOld] = undefined // 移动node节点 canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element // 创建新的dom节点 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] }
通过以上分析,给vdom上添加key属性后,遍历diff的过程中,当起始点, 结束点的搜寻及diff出现还是无法匹配的情况下时,就会用key来作为唯一标识,来进行diff,这样就可以提高diff效率。
带有Key属性的vnode的diff过程可见下图:
注意在第一轮的diff过后oldCh上的B节点被删除了,但是newCh上的B节点上elm属性保持对oldCh上B节点的elm引用。