高级前端开发带你搞懂vue的diff算法
网上看了一些diff的算法,但是感觉看完之后,还是那么的一知半解,为什么一个简单的diff算法,不能直接画个流程图就简单的明了了呢,说动就动,下面的是本人基于vue版本2.6.11源码为各位读友进行的解析
Vue的diff流程图
流程前说明
- 由于diff的过程是对vnode(虚拟dom)树进行层级比较,所以以同一层级作为例子
- 下面将旧节点列表的起始和终止节点称为OS(OldStarVnode)和OE(OldEndVnode),用index标志遍历过程OS和OE的变化。即OS和OE的index称为OSIndex和OEIndex。同理得新节点的为NS和NE,NSIndex和NEIndex,如下图
主流程
如下图:
文字版描述一下就是:
- 判断是否遍历完,未遍历则开始2,否则,如果遍历完了旧节点列表,则未遍历的新节点则创建并且增加到节点列表,如果遍历完了新节点列表,则未遍历的旧节点在节点列表里面删除
- 对旧节点的OS和OE进行判空,如果为空,则跳过该节点,继续从1开始;否则继续3
- 对OS,OE,NS,NE进行两两比较,如果相等,则更新节点并且指针向下一个移动,继续从1开始;否则继续4
- 判断NS是否有key,有key则判断NS是否在旧节点列表里面找到key一样的进行更新;否则创建NS并且插入节点列表
updateChildren进行diff算法源码
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
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, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newendidx if isundefoldstartvnode oldstartvnode='oldCh[++oldStartIdx]' vnode has been moved left else if isundefoldendvnode oldendvnode='oldCh[--oldEndIdx]' else if samevnodeoldstartvnode newstartvnode patchvnodeoldstartvnode newstartvnode insertedvnodequeue newch newstartidx oldstartvnode='oldCh[++oldStartIdx]' newstartvnode='newCh[++newStartIdx]' else if samevnodeoldendvnode newendvnode patchvnodeoldendvnode newendvnode insertedvnodequeue newch newendidx oldendvnode='oldCh[--oldEndIdx]' newendvnode='newCh[--newEndIdx]' else if samevnodeoldstartvnode newendvnode vnode moved right patchvnodeoldstartvnode newendvnode insertedvnodequeue newch newendidx canmove nodeops.insertbeforeparentelm oldstartvnode.elm nodeops.nextsiblingoldendvnode.elm oldstartvnode='oldCh[++oldStartIdx]' newendvnode='newCh[--newEndIdx]' else if samevnodeoldendvnode newstartvnode vnode moved left patchvnodeoldendvnode newstartvnode insertedvnodequeue newch newstartidx canmove nodeops.insertbeforeparentelm oldendvnode.elm oldstartvnode.elm oldendvnode='oldCh[--oldEndIdx]' newstartvnode='newCh[++newStartIdx]' else if isundefoldkeytoidx oldkeytoidx='createKeyToOldIdx(oldCh,' oldstartidx oldendidx idxinold='isDef(newStartVnode.key)' oldkeytoidxnewstartvnode.key : findidxinoldnewstartvnode oldch oldstartidx oldendidx if isundefidxinold new element createelmnewstartvnode insertedvnodequeue parentelm oldstartvnode.elm false newch newstartidx else vnodetomove='oldCh[idxInOld]' if samevnodevnodetomove newstartvnode patchvnodevnodetomove newstartvnode insertedvnodequeue newch newstartidx oldchidxinold='undefined' canmove nodeops.insertbeforeparentelm vnodetomove.elm oldstartvnode.elm else same key but different element. treat as new element createelmnewstartvnode 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(oldCh, oldStartIdx, oldEndIdx)
}
}
附,源码中部分工具函数的解释:
isUndef 对节点进行判空
function isUndef (v) {
return v === undefined || v === null
}
sameVnode对节点进行判断是否相等
- 判断新旧节点的key
- 判断新旧节点的属性(tag,isComment表示是否是注释节点,isDef表示是否为非空节点,sameInputType表示是否同个Input节点)是否一致
- 判断新旧节点的加载函数asyncFactory是否一致
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)
)
)
)
}
patchVnode更新节点
patchVnode更新节点主要做以下事情,代码比较长就不贴了,影响读者,需要可以直接阅读源码:
- 判断vnode和oldvnode是否相等,相等直接返回
- 处理静态节点的情况
- 对vnode如果是可patch的情形进行调用update
- 对vnode进行判断是否是根节点(即文本节点),如果是,则进行5,否则则对其子节点进行遍历更新
- 判断vnode和oldvnode文本是否一样: 不一样则替换节点文本