一直不想寫 vue diff 的過程,因爲 這一塊網絡上講的人實在是太多了,這裏寫了也只不過是拾人牙慧罷了,但是不寫吧,又覺得心癢癢的,畢竟前面寫了一篇 react 源碼中 對 key 的使用 的
首先是幾個工具函數
// 這裏用來判斷 一個值是否是 未定義的
export function isDef(v: any): boolean % checks {
return v !== undefined && v !== null
}
// 對比 是不是 新節點
// 這裏和之前的區別是, 沒有設置 key 的情況下,key 被認爲是相等的
function findIdxInOld(node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
// 兩個 vnode 是否相同
function sameVnode(a, b) {
return (
// 需要注意的是 null === null 爲 true
// 也就是說,如果 沒有設置 key 的話,那麼就默認爲 兩個節點的key 是一致的
// 對比於 react 的diff ,這裏的判斷 多出了 tag、comment 和 input 的判斷
// 而 react 則着重於 $$type 的判斷
// 這裏 react 明顯的 對於 jsx 的分類 更傾向於 自己的判斷,而 vue 則是 傾向於 用戶的輸入
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
// 是否是 同一個類型的 input 標籤,這個很好理解
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
// 是否是 相同類型的 input 節點,比如 radio,text,checkbox等
function sameInputType(a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
真正 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 <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 而這裏,也是和 react diff 的最大的不同之處了,
// react 對於新舊節點只做了順序的 查找 相同節點,而 vue 還做了 前後節點的 對比
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// 新舊節點的 排頭 對比,相等,則複用
} else 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]
} else {
// 這裏是 爲 老節點的 key 和 位置 做一個 對應關係
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 查找 在 老節點的 key 對應 map 中有沒有 可以複用的節點
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 沒有可以複用的,那就 創建一個
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
// 然後再對比 input 的type 和 tag 等屬性,一樣的話,吧老節點 對應的內容制空,複用節點
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 雖然 key 相等,但是節點類型不同,創建新節點
createElm(newStartVnode, 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)
}
}
所以 react 和 vue 的 diff 之間的異同?
當 key 爲 null,也就是沒有設置 key 的時候,兩個都是當作 相同的節點來進行的
react 和 vue 中對應 兩個節點是否 可以複用 有着不同的判斷
react 中的 複用,指的是 key 相等,然後 type 相等,這裏的 type 是在創建的時候,根據節點類型 設置的
而 vue 的 複用,指的是 key 相等,節點的 tag 類型相等,input 的 type 相等
而 vue diff 中的 優化,指的就是 對 排頭排尾 的比較了,