也看過其餘講vue diff過程的文章,可是感受都只是講了其中的一部分(對比方式),沒有對其中細節的部分作詳細的講解,如vue
patchVnode
是作了什麼?爲何的有的緊接着要進行dom操做,有的沒有?insertedVnodeQueue
又是何用?爲什麼一直帶着?這裏並不會直接就開始講diff,爲了讓你們能瞭解到diff的詳細過程,所在開始核心部分以前,有些簡單的概念和流程須要提早說明一下,固然最好是但願你已經對vue源碼patch這部分有些瞭解。node
因爲核心是說明diff的過程,因此會先把diff涉及到的核心概念簡單說明一下,對於這些若仍有疑問能夠在評論區留言:算法
簡單的說就是真實 dom 的描述對象,這也是vue的特色之一 - virtual dom。因爲原生的dom結構過於複雜,當須要獲取並瞭解節點信息的時候,並不須要操做複雜的 dom,相應的vue 是先用其描述對象進行分析(diff 對比也就是vnode的對比),而後再反應到真實的 dom。數組
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
functionalContext: Component | void; // real context vm for functional nodes
functionalOptions: ?ComponentOptions; // for SSR caching
functionalScopeId: ?string; // functioanl scope id support
constructor () {
...
}
}
複製代碼
須要注意的是後面會涉及到的幾個屬性:app
children
和parent
經過這個創建其vnode之間的層級關係,對應的也就是真實dom的層級關係text
若是存在值,證實該vnode對應的就是一個文件節點,跟children是一個互斥的關係,不可能同時有值tag
代表當前vnode,對應真實 dom 的標籤名,如‘div’、‘p’elm
就是當前vnode對應的真實的dom閱讀源碼中複雜函數的小技巧:看‘一頭’‘一尾’。‘頭’指的的入參,提煉出能看懂和能理解的參數(oldVnode
、vnode
、parentElm
),‘尾’指的是函數的處理結果,這個返回的elm
。因此能夠根據‘頭尾’總結下,patch
完成以後,新的vnode
上會對應生成elm
,也就是真實的 dom,且是已經掛載到parentElm
下的dom。簡單的來講,如vue 實例初始化、數據更改致使的頁面更新等,都須要通過patch
方法來生成elm。dom
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// ...
const insertedVnodeQueue = []
// ...
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
}
// ...
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
}
// ...
return vnode.elm
}
複製代碼
patch 的過程(除去邊界條件)主要會有三種 case:async
不存在 oldVnode,則進行createElm
函數
存在 oldVnode 和 vnode,可是 sameVnode
返回 false, 則進行createElm
測試
存在 oldVnode 和 vnode,可是 sameVnode
返回 true, 則進行patchVnode
ui
上面提到了sameVnode
,代碼以下:
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)
)
)
)
}
複製代碼
簡單的舉個的case,好比以前是一個<div>
標籤,因爲邏輯的變更,變爲<p>
標籤了,則sameVnode
會返回false
(a.tag === b.tag
返回 false)。因此sameVnode
代表的是,知足以上條件就是同一個元素,纔可進行patchVnode
。反過來理解就是,只要以上任意一個發生改變,則無需進行pathchVnode
,直接根據vnode
進行createElm
便可。
注意,sameVnode
返回true,不能說明是同一個vnode,這裏的相同是指當前的以上指標一致,他們的children可能發生了變化,仍需進行patchVnode
進行更新。
由patch
方法,咱們知道patchVnode
方法和createElm
的方法最終的處理結果同樣,就是生成或更新了當前vnode對應的dom。
通過上面的分析,總結下,就是當須要生成 dom,且先後vnode進行sameVnode
爲true
的狀況下,則進行patchVnode
。
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// ...
const elm = vnode.elm = oldVnode.elm
// ...
const oldCh = oldVnode.children
const ch = vnode.children
// ...
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 具體是何種狀況下會走到這個邏輯???
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// ...
}
複製代碼
以上是patchVnode
的部分代碼,展現出來的這部分邏輯,也是patchVnode
的核心處理邏輯。
以上代碼,充斥大量的if
else
,你們能夠思考幾個問題?
case
會進入到removeVnodes
的邏輯?這其實也是我在閱讀的時候思考的問題,最終我採用瞭如下的方式(對着代碼繪製表格)來解決這種複雜的if
else
邏輯的解讀:
oldVnode.text | oldCh | !oldCh | |
---|---|---|---|
vnode.text | setTextContent | setTextContent | setTextContent |
ch | addVnodes | updateChildren | addVnodes |
!ch | setTextContent | removeVnodes | setTextContent |
對應着表格,而後對應着代碼,相信你能找到答案。
通過上面的分析,只有在oldCh
和ch
都存在的狀況下才會執行updateChildren
,此時入參是oldCh
和ch
,因此能夠知道的是,updateChildren
進行的是同層級下的children
的更新比較,也就是‘傳說中的’diff了。
開始分析以前,能夠思考下:若如今js來操做原生dom的一個<ul>
列表,固然這個列表也是用原生的js來實現的,如今若是其中的數據順序發生了變化,第一條要排到末尾或具體的某個位置,或者有新增數據、刪除數據等,該如何操做。
let listData = [
'測試數據1',
'測試數據2',
'測試數據3',
'測試數據4',
'測試數據5',
]
let ulElm = document.createElement('ul');
let liStr = '';
for(let i = 0; i < listData.length; i++){
liStr += `<li>${listData[i]}</li>`
}
ulElm.append(liStr)
document.body.innerHTML = ''
document.body.append(ulElm)
複製代碼
這個時候因爲變化的不肯定性,不但願在業務代碼邏輯中維護繁瑣的insertBefore
、appendChild
、removeChild
、replaceChild
,立馬能想到的粗暴的解決方式是,咱們拿到最新的listData
,把上面面建立的流程再走一遍。
然而vue採起的是diff算法,簡單的說就是:
listData
_render
操做,獲得新的vnodeupdateChildren
操做(diff),進行最小的變更updateChildren
代碼以下:
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
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)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
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)
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)
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
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)
}
}
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(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
以前分析了,oldCh
和ch
表示的是同層級的vnode的列表,也就是兩個數組
開始以前定義了一系列的變量,分別以下:
oldStartIdx
開始指針,指向oldCh中待處理部分的頭部,對應的vnode也就是oldStartVnode
oldEndIdx
結束指針,指向oldCh中待處理部分的尾部,對應的vnode也就是oldEndVnode
newStartIdx
開始指針,指向ch中待處理部分的頭部,對應的vnode也就是newStartVnode
newEndIdx
結束指針,指向ch中待處理部分的尾部,對應的vnode也就是newEndVnode
oldKeyToIdx
是一個map,其中key就是常在for循環中寫的v-bind:key
的值,value 對應的就是當前vnode,也就是能夠經過惟一的key,在map中找到對應的vnodeupdateChildren
使用的是while循環來更新dom的,其中的退出條件就是!(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
,換種理解方式:oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
,什麼意思呢,就是隻要有一個發生了‘交叉’(下面的例子會出現交叉)就退出循環。
原有的oldCh的順序是 A 、B、C、D、E、F、G,更新後成ch的順序 F、D、A、H、E、C、B、G。
爲了更好理解後續的round,開始以前先看下相關符合標記的說明
round1: 對比順序:A-F -> G-G,匹配成功,而後:
patchVnode
的操做,更新oldEndVnode
G和newEndVnode
G的elmoldEndIdx--
newEndIdx--
round2: 對比順序:A-F -> F-B -> A-B -> F-F,匹配成功,而後:
patchVnode
的操做,更新oldEndVnode
F和newEndVnode
F的elmoldEndIdx--
newStartIdx++
oldStartVnode
在dom中所在的位置A,而後在其前面插入更新過的F的elmround3: 對比順序:A-D -> E-B -> A-B -> E-D,仍未成功,取D的key,在oldKeyToIdx
中查找,找到對應的D,查找成功,而後:
vnodeToMove
patchVnode
的操做,更新vnodeToMove
D和newStartVnode
D的elmnewStartIdx++
undefined
oldStartVnode
A的elm對應的節點,而後在其前面插入更新過的D的elmround4: 對比順序:A-A,對比成功,而後:
patchVnode
的操做,更新oldStartVnode
A和newStartVnode
A的elmoldStartIdx++
newStartIdx++
round5: 對比順序:B-H -> E-B -> B-B ,對比成功,而後:
patchVnode
的操做,更新oldStartVnode
B和newStartVnode
B的elmoldStartIdx++
newEndIdx--
oldEndVnode
E的elm的nextSibling
節點(即G的elm),而後在其前面插入更新過的B的elmround6: 對比順序:C-H -> E-C -> C-C ,對比成功,而後(同round5):
patchVnode
的操做,更新oldStartVnode
C和newStartVnode
C的elmoldStartIdx++
newEndIdx--
oldEndVnode
E的elm的nextSibling
節點(即剛剛插入的B的elm),而後在其前面插入更新過的C的elmround7: 獲取oldStartVnode失敗(由於round3的步驟4),而後:
oldStartIdx++
round8: 對比順序:E-H、E-E,匹配成功,而後(同round1):
patchVnode
的操做,更新oldEndVnode
E和newEndVnode
E的elmoldEndIdx--
newEndIdx--
last round8以後oldCh提早發生了‘交叉’,退出循環。
last:newEndIdx+1
對應的元素AnewStartIdx
-newEndIdx
中的vnode)則爲新增的部分,無需patch,直接進行createElm
updateChildren
傳入的parentElm
,即父vnode的elmpatchVnode
,往前看patchVnode
的部分,其處理的結果就是oldVnode.elm和vnode.elm獲得了更新insertBefore
,重點是要先找到插入的地方每個round(以上例子中涉及到的)作的事情以下(優先級從上至下):
oldStartVnode
則移動(參照round6)oldKeyToIdx
中根據newStartVnode
的能夠進行查找,成功則更新並移動(參照round3) (更新並移動:patchVnode更新對應vnode的elm,並移動指針)關於插入的問題,爲什麼有的緊接着進行的dom操做,有的沒有?什麼時候在oldStartVnode
的elm前插,什麼時候在oldEndVnode
的elm的nextSibling
前插?
這裏只要記住,oldCh
和ch
都是參照物,其中,ch
是咱們的目標順序,而oldCh
是咱們用來了解當前dom順序的參照,也就是開篇提到的vnode的介紹。因此整個diff過程,就是對比oldCh
和ch
,確認當前round,oldCh
如何移動更靠近ch
,因爲oldCh
中待處理的部分仍在dom中,因此能夠根據oldCh
中的oldStartVnode
的elm和 oldEndVnode
的elm的位置,來肯定匹配成功的元素該如何插入。
oldStartVnode
位置正是如今的位置,無需移動,進行patchVnode
更新便可oldEndVnode
與newSatrtVnode
匹配成功,這裏注意成功的是newSatrtVnode
,因此是在待處理dom的頭部前插。如round2,當前待處理的部分,也就是oldCh
中黑塊的部分,頭部也就是oldStartVnode
。也就是在oldStartVnode
的elm前面插入newSatrtVnode
的elm。oldStartVnode
與newEndVnode
匹配成功,這裏注意成功的是newEndVnode
,因此是在待處理dom的尾部插入(就是尾部元素的下一個元素前插)。如round5,當前待處理的部分,也就是oldCh
中黑塊的部分,尾部也就是oldEndVnode
。也就是先找到oldEndVnode
的elm的nextSibling
前面插入newEndVnode
的elm。(這裏有提到‘待處理塊’,具體你們能夠看示意圖,注意oldCh
中的待處理塊部分和dom中待處理的部分)
以上已經包含updateChildren
中大部分的內容了,固然還有部分沒有涉及到的就不一一說明的,具體的你們能夠對着源碼,找個實例走整個的流程便可。
最後還有一個問題沒回答,insertedVnodeQueue
有何用?爲啥一直帶着?
這部分涉及到組件的patch的過程,這裏能夠簡單說下:組件的$mount
函數以後以後並不會當即觸發組件實例的mounted
鉤子,而是把當前實例push
到insertedVnodeQueue
中,而後在patch的倒數第二行,會執行invokeInsertHook
,也就是觸發全部組件實例的insert
的鉤子,而組件的insert
鉤子函數中才會觸發組件實例的mounted
鉤子。比方說,在patch的過程當中,patch了多個組件vnode,他們都進行了$mount
即生成dom,但沒有當即觸發$mounted
,而是等整個patch
完成,再逐一觸發。