前言
上一篇《vue異步更新流程梳理》 梳理了數(shù)據(jù)從賦值到更新到視圖的整體流程采幌。但是最后的步驟vm._update(vm._render()) 只是粗略的提了一嘴嫌术,現(xiàn)在就仔細(xì)的研究它內(nèi)部的細(xì)節(jié)验靡,搞清楚patch流程和diff原理是我們看源碼的重中之重谦纱。
更新過程
- 代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">{{value}}</div>
<script src="../dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
value: 1
}
},
mounted () {
this.value = 2
}
})
</script>
</body>
</html>
-
在vue實(shí)例調(diào)用$mount的時(shí)候,就已經(jīng)把updateComponent 方法通過new Watcher(vm, updateComponent)傳入到渲染watcher里面届氢, 且掛在watcher.getter上独泞,得到一個(gè)渲染watcher, 渲染watcher在以后每次響應(yīng)式數(shù)據(jù)更新都會執(zhí)行watcher.getter 即 updateComponent 方法。
mount.png
當(dāng)更新數(shù)據(jù)的時(shí)候就會執(zhí)行這個(gè)updateComponent方法信不,即方法里面的vm._update(vm._render())嘲叔,vm.render() 得到一個(gè)vnode,那么vm._update到底干什么? 進(jìn)去看看
其實(shí)就是判斷是進(jìn)行初始化流程(initial render) 或者是更新流程(updates),但是都是調(diào)用了 vm.__patch__ 函數(shù)抽活;兩者的區(qū)別是
- 初始化的第一個(gè)參數(shù)是el節(jié)點(diǎn)硫戈,也就是例子中真實(shí)的dom節(jié)點(diǎn)#app
- 而更新流程的第一個(gè)參數(shù)就是prevVnode, 這個(gè)prevVnode其實(shí)是一個(gè)vnode, 是更新前的vnode
至此,無論是初始化還是更新都是靠patch來完成的 下硕,我們只需要看update流程就可以了丁逝。進(jìn)入patch內(nèi)部
patch函數(shù)主要接收oldVnode 與 vnode兩個(gè)參數(shù),其實(shí)就是新舊兩棵虛擬樹梭姓。這里經(jīng)過判斷條件 !isRealElement && sameVnode(oldVnode, vnode)霜幼,不是真實(shí)節(jié)點(diǎn) 且是相同的vnode,進(jìn)入patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); 我們只要關(guān)注oldVnode, vnode這兩個(gè)參數(shù)即可誉尖。
按照我們的例子罪既,此時(shí)的oldVnode 與 vnode分別是
oldVnode: {
tag: 'div',
key: undefined,
elm: div#app, // 這是該vnode節(jié)點(diǎn)映射對應(yīng)的真實(shí)dom節(jié)點(diǎn)
children: [
{
tag: undefined,
key: undefined,
elm: text // 文本節(jié)點(diǎn)
text: '1'
}
]
}
vnode: {
tag: 'div',
key: undefined,
elm: div#app, // 這是該vnode節(jié)點(diǎn)映射對應(yīng)的真實(shí)dom節(jié)點(diǎn)
children: [
{
tag: '',
key: undefined,
elm: undefined, // 注意這里是undefined,此時(shí)還沒有diff
text: '2'
}
]
}
此處只列出關(guān)鍵屬性tag, key, elm, children,elm铡恕,還有很多其他的屬性沒有列出琢感。真實(shí)的虛擬樹節(jié)點(diǎn)應(yīng)該是如下圖
我們能看出 兩個(gè)vnode之間就是children[0]的不同:
oldVnode.children[0].text === '1',
vnode.children[0].text === '2'
追蹤流程發(fā)現(xiàn)探熔,我們進(jìn)入oldVnode 與 vnode的children進(jìn)行對比驹针,在updateChildren函數(shù)中。
我們先不去看updateChildren的邏輯诀艰,繼續(xù)看patchVnode這個(gè)函數(shù)其他的邏輯分支柬甥,得出oldVnode 與 vnode的對比流程:
- 拿出兩者的children: oldCh, ch
- 如果vnode是元素節(jié)點(diǎn)
2.1,如果oldCh, ch兩者都存在且不相同其垄,進(jìn)入updateChildren流程
2.2苛蒲,否則如果只是ch 存在,oldCh不存在捉捅,那么直接操作真實(shí)dom, 添加ch節(jié)點(diǎn)
2.3撤防,否則如果只是oldCh 存在狞膘,ch不存在审轮,那么直接操作真實(shí)dom, 刪除oldCh節(jié)點(diǎn)
2.4,否則如果只是oldCh是文本袒餐, ch不存在无牵,直接操作真實(shí)dom設(shè)置文本節(jié)點(diǎn)為空 '' - 如果vnode是文本節(jié)點(diǎn)漾肮,直接操作真實(shí)dom, 設(shè)置文本節(jié)點(diǎn)為 vnode.text
總結(jié):patchVnode這個(gè)方法的主要作用是對比兩個(gè)虛擬節(jié)點(diǎn)過程中去更新真實(shí)dom
接下來我們進(jìn)入updateChildren流程,這是兩個(gè)children的對比茎毁,看一下這個(gè)函數(shù)的定義
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)
}
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 {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
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]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
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)
}
}
函數(shù)解讀:
- 參數(shù) parentElm:真實(shí)的dom元素克懊,做為父節(jié)點(diǎn),供更新children時(shí)去插入
- 參數(shù) oldCh:老的虛擬樹的children
- 參數(shù) newCh: 新的虛擬樹的children
- oldStartIdx:老數(shù)組的開始的下標(biāo)index
- newStartIdx: 新數(shù)組的開始的下標(biāo)index
- oldEndIdx: 老數(shù)組的結(jié)束的下標(biāo)index
- oldStartVnode : 老數(shù)組的開始的節(jié)點(diǎn)
- oldEndVnode : 老數(shù)組的結(jié)束的節(jié)點(diǎn)
- newEndIdx :新數(shù)組的結(jié)束的下標(biāo)index
- newStartVnode :新數(shù)組的開始的節(jié)點(diǎn)
- newEndVnode :新數(shù)組的結(jié)束的節(jié)點(diǎn)
- oldKeyToIdx:老數(shù)組的節(jié)點(diǎn)的key與index的一個(gè)映射map
- idxInOld:老數(shù)組的某個(gè)節(jié)點(diǎn)的index
- vnodeToMove:待移動的節(jié)點(diǎn)七蜘,也就是在oldKeyToIdx中找到key的可復(fù)用的節(jié)點(diǎn)
- refElm: 一個(gè)真實(shí)的節(jié)點(diǎn)谭溉,用來做參考位置的,表示從哪里開始添加新的dom節(jié)點(diǎn)
下面是兩個(gè)數(shù)組進(jìn)行diff的流程橡卤,也就是diff算法
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 {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
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]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
diff解讀:
新舊兩個(gè)數(shù)組扮念,都有雙端指針,兩端指針向中間靠攏碧库,直到某個(gè)數(shù)組的兩端指針相交則退出循環(huán)柜与。
在這個(gè)過程中,會先判斷是否有以下四種情況
- 舊首 - 新首 是同一個(gè)節(jié)點(diǎn)嵌灰,互相比較弄匕,深度遞歸進(jìn)行patchVnode流程,
- 舊尾 - 新尾 是同一個(gè)節(jié)點(diǎn)沽瞭,互相比較迁匠,深度遞歸進(jìn)行patchVnode流程
- 舊首 - 新尾 是同一個(gè)節(jié)點(diǎn),互相比較驹溃,深度遞歸進(jìn)行patchVnode流程
- 舊尾 - 新首 是同一個(gè)節(jié)點(diǎn)柒瓣,互相比較,深度遞歸進(jìn)行patchVnode流程
如果不符合這4種情況吠架,那就基于舊數(shù)組遍歷一次芙贫,拿到每個(gè)節(jié)點(diǎn)的key和index,就是oldKeyToIdx: {key1: 0, key2: 1}這種情況。然后去新數(shù)組首個(gè)節(jié)點(diǎn)開始匹配傍药,匹配到就進(jìn)行遞歸patchVnode流程磺平,沒匹配到就進(jìn)行創(chuàng)建新節(jié)點(diǎn),插入到真實(shí)dom節(jié)點(diǎn)里面去拐辽。
當(dāng)循環(huán)結(jié)束拣挪,此時(shí)要么是舊數(shù)組相交,要么是新數(shù)組相交俱诸,只有這兩種情況:
- 舊數(shù)組相交菠劝,說明新數(shù)組還沒有相交,那么要根據(jù)相交的位置插入新數(shù)組剩余的未遍歷到節(jié)點(diǎn)
- 新數(shù)組相交睁搭,說明舊數(shù)組還沒有相交赶诊,那么要刪除舊數(shù)組剩余的未遍歷到的節(jié)點(diǎn)
至此diff流程結(jié)束笼平。
總結(jié)
兩個(gè)虛擬樹進(jìn)行對比:
patch(oldVnode, vnode) -> patchVnode(oldVnode, vnode) -> updataChildren(oldCh, newCh)
在updataChildren(oldCh, newCh)的過程中也會進(jìn)行 patchVnode(oldVnode, vnode) ,如此虛擬樹深度優(yōu)先遞歸diff完成舔痪。
更加詳細(xì)直觀的圖看此鏈接
https://www.processon.com/view/5e809004e4b08e4e2447d02e