首先理解VNode對(duì)象
一個(gè)VNode的實(shí)例對(duì)象包含了以下屬性,參見源碼src/vdom/vnode.js
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
其中幾個(gè)比較重要的屬性:
-
tag
: 當(dāng)前節(jié)點(diǎn)的標(biāo)簽名 -
data
: 當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)對(duì)象响疚,具體包含哪些字段可以參考vue源碼types/vnode.d.ts
中對(duì)VNodeData
的定義 -
children
: 數(shù)組類型智什,包含了當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn) -
text
: 當(dāng)前節(jié)點(diǎn)的文本以舒,一般文本節(jié)點(diǎn)或注釋節(jié)點(diǎn)會(huì)有該屬性 -
elm
: 當(dāng)前虛擬節(jié)點(diǎn)對(duì)應(yīng)的真實(shí)的dom節(jié)點(diǎn) -
key
: 節(jié)點(diǎn)的key屬性实夹,用于作為節(jié)點(diǎn)的標(biāo)識(shí),有利于patch的優(yōu)化
比如尘颓,定義一個(gè)vnode痘系,它的數(shù)據(jù)結(jié)構(gòu)是:
{
tag: 'div'
data: {
id: 'app',
class: 'page-box'
},
children: [
{
tag: 'p',
text: 'this is demo'
}
]
}
通過一定的渲染函數(shù)艺智,最后渲染出的實(shí)際的dom結(jié)構(gòu)就是:
<div id="app" class="page-box">
<p>this is demo</p>
</div>
VNode對(duì)象是JS用對(duì)象模擬的DOM節(jié)點(diǎn),通過渲染這些對(duì)象即可渲染成一棵dom樹褐筛。
patch
我對(duì)patch的理解就是對(duì)內(nèi)容已經(jīng)變更的節(jié)點(diǎn)進(jìn)行修改的過程
當(dāng)model中的響應(yīng)式的數(shù)據(jù)發(fā)生了變化腾它,這些響應(yīng)式的數(shù)據(jù)所維護(hù)的dep數(shù)組便會(huì)調(diào)用dep.notify()方法完成所有依賴遍歷執(zhí)行的工作,這里面就包括了視圖的更新即updateComponent方法死讹。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
完成視圖的更新工作事實(shí)上就是調(diào)用了vm._update方法瞒滴,這個(gè)方法接收的第一個(gè)參數(shù)是剛生成的Vnode(vm._render()會(huì)生成一個(gè)新的Vnode)
vm._update方法主要調(diào)用了vm._patch_() 方法,這也是整個(gè)virtaul-dom當(dāng)中最為核心的方法赞警,主要完成了prevVnode和vnode的diff過程并根據(jù)需要操作的vdom節(jié)點(diǎn)打patch妓忍,最后生成新的真實(shí)dom節(jié)點(diǎn)并完成視圖的更新工作。
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 當(dāng)oldVnode不存在時(shí)
if (isUndef(oldVnode)) {
// 創(chuàng)建新的節(jié)點(diǎn)
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
}
}
}
在當(dāng)oldVnode不存在的時(shí)候愧旦,這個(gè)時(shí)候是root節(jié)點(diǎn)初始化的過程世剖,因此調(diào)用了createElm(vnode, insertedVnodeQueue, parentElm, refElm)方法去創(chuàng)建一個(gè)新的節(jié)點(diǎn)。而當(dāng)oldVnode是vnode且sameVnode(oldVnode, vnode)2個(gè)節(jié)點(diǎn)的基本屬性相同笤虫,那么就進(jìn)入了2個(gè)節(jié)點(diǎn)的patch以及diff過程旁瘫。
(在對(duì)oldVnode和vnode類型判斷中有個(gè)sameVnode方法,這個(gè)方法決定了是否需要對(duì)oldVnode和vnode進(jìn)行diff及patch的過程琼蚯。如果2個(gè)vnode的基本屬性存在不一致的情況酬凳,那么就會(huì)直接跳過diff的過程,進(jìn)而依據(jù)vnode新建一個(gè)真實(shí)的dom遭庶,同時(shí)刪除老的dom節(jié)點(diǎn))
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)
)
}
patch過程主要調(diào)用了patchVnode
(src/core/vdom/patch.js)方法進(jìn)行的:
if (isDef(data) && isPatchable(vnode)) {
// cbs保存了hooks鉤子函數(shù): 'create', 'activate', 'update', 'remove', 'destroy'
// 取出cbs保存的update鉤子函數(shù)宁仔,依次調(diào)用,更新attrs/style/class/events/directives/refs等屬性
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
更新真實(shí)dom節(jié)點(diǎn)的data屬性峦睡,相當(dāng)于對(duì)dom節(jié)點(diǎn)進(jìn)行了預(yù)處理的操作
接下來:
...
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 如果vnode沒有文本節(jié)點(diǎn)
if (isUndef(vnode.text)) {
// 如果oldVnode的children屬性存在且vnode的屬性也存在
if (isDef(oldCh) && isDef(ch)) {
// updateChildren翎苫,對(duì)子節(jié)點(diǎn)進(jìn)行diff
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果oldVnode的text存在,那么首先清空text的內(nèi)容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 然后將vnode的children添加進(jìn)去
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 刪除elm下的oldchildren
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有子節(jié)點(diǎn)榨了,而vnode沒有煎谍,那么就清空這個(gè)節(jié)點(diǎn)
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點(diǎn)的文本元素
nodeOps.setTextContent(elm, vnode.text)
}
這個(gè)patch的過程又分為幾種情況:
1.當(dāng)vnode
的text為空龙屉,即不是文本節(jié)點(diǎn)時(shí)呐粘。
- 如果
oldVnode
和新節(jié)點(diǎn)vnode
都有子節(jié)點(diǎn)。
則調(diào)用updateChildren( ),對(duì)子節(jié)點(diǎn)進(jìn)行diff - 如果只有新節(jié)點(diǎn)
vnode
有子節(jié)點(diǎn)
則判斷oldVnode
是否是文本節(jié)點(diǎn)事哭,如果是文本節(jié)點(diǎn)漫雷,則首先清空真實(shí)節(jié)點(diǎn)的text的內(nèi)容。然后把新節(jié)點(diǎn)的children添加到elm中鳍咱。 - 如果只有
oldVnode
有子節(jié)點(diǎn)時(shí)
則調(diào)用removeVnodes()刪除elm下的oldVnode
的children降盹。 - 如果
oldVnode
和新節(jié)點(diǎn)vnode
都沒有子節(jié)點(diǎn),且oldVnode
是文本節(jié)點(diǎn)
則清空真實(shí)節(jié)點(diǎn)的text的內(nèi)容谤辜。
2.當(dāng)vnode
的text存在蓄坏,即是文本節(jié)點(diǎn)時(shí)
則設(shè)置真實(shí)節(jié)點(diǎn)的text內(nèi)容為vnode
的text內(nèi)容。
diff過程
我對(duì)diff的理解就是遍歷兩棵不同的虛擬樹丑念,如果其中有的節(jié)點(diǎn)不同涡戳,則進(jìn)行patch。
上個(gè)函數(shù)的updateChildren
(src/core/vdom/patch.js)方法就是diff過程脯倚,它也是整個(gè)diff
過程中最重要的環(huán)節(jié):
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 為oldCh和newCh分別建立索引渔彰,為之后遍歷的依據(jù)
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, elmToMove, refElm
// 直到oldCh或者newCh被遍歷完后跳出循環(huán)
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)
// 插入到老的開始節(jié)點(diǎn)的前面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果以上條件都不滿足,那么這個(gè)時(shí)候開始比較key值推正,首先建立key和index索引的對(duì)應(yīng)關(guān)系
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
// 如果idxInOld不存在
// 1. newStartVnode上存在這個(gè)key,但是oldKeyToIdx中不存在
// 2. newStartVnode上并沒有設(shè)置key屬性
if (isUndef(idxInOld)) { // New element
// 創(chuàng)建新的dom節(jié)點(diǎn)
// 插入到oldStartVnode.elm前面
// 參見createElm方法
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
// 將找到的key一致的oldVnode再和newStartVnode進(jìn)行diff
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
// 移動(dòng)node節(jié)點(diǎn)
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
// 創(chuàng)建新的dom節(jié)點(diǎn)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
// 如果最后遍歷的oldStartIdx大于oldEndIdx的話
if (oldStartIdx > oldEndIdx) { // 如果是老的vdom先被遍歷完
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// 添加newVnode中剩余的節(jié)點(diǎn)到parentElm中
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) { // 如果是新的vdom先被遍歷完恍涂,則刪除oldVnode里面所有的節(jié)點(diǎn)
// 刪除剩余的節(jié)點(diǎn)
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
代碼中,oldStartIdx植榕,oldEndIdx是遍歷oldCh(oldVnode的子節(jié)點(diǎn))
的索引
newStartIdx再沧,newEndIdx是遍歷newCh(vnode的子節(jié)點(diǎn))
的索引
diff遍歷的過程如下: (節(jié)點(diǎn)屬性中不帶key的情況)
遍歷完的條件就是oldCh或者newCh的startIndex >= endIndex
首先先判斷oldCh
的起始節(jié)點(diǎn)oldStartVnode
和末尾節(jié)點(diǎn)oldEndVnode
是否存在,如果不存在尊残,則oldCh
的起始節(jié)點(diǎn)向后移動(dòng)一位炒瘸,末尾節(jié)點(diǎn)向前移動(dòng)一位。
如果存在寝衫,則每一輪diff都進(jìn)行比較如下比較:
-
sameVnode(oldStartVnode, newStartVnode)
判斷老節(jié)點(diǎn)的初節(jié)點(diǎn)和新節(jié)點(diǎn)的初節(jié)點(diǎn)是否是同一類型顷扩,如果是,則對(duì)它們兩個(gè)進(jìn)行patchVnode(patch過程).兩個(gè)節(jié)點(diǎn)初節(jié)點(diǎn)分別向后移動(dòng)一位竞端。 - 如果1不滿足屎即,
sameVnode(oldEndVnode, newEndVnode)
判斷老節(jié)點(diǎn)的尾節(jié)點(diǎn)和新節(jié)點(diǎn)的尾節(jié)點(diǎn)是否是同一類型,如果是事富,則對(duì)它們兩個(gè)進(jìn)行patchVnode(patch過程).兩個(gè)節(jié)點(diǎn)尾節(jié)點(diǎn)分別向前移動(dòng)一位。 - 如果2也不滿足乘陪,則sameVnode(oldStartVnode, newEndVnode)
判斷老節(jié)點(diǎn)的初節(jié)點(diǎn)和新節(jié)點(diǎn)的尾節(jié)點(diǎn)是否是同一類型统台,如果是,則對(duì)它們兩個(gè)進(jìn)行patchVnode(patch過程).老節(jié)點(diǎn)的初節(jié)點(diǎn)向后移動(dòng)一位啡邑,新節(jié)點(diǎn)尾節(jié)點(diǎn)向前移動(dòng)一位贱勃。 - 如果3也不滿足,則sameVnode(oldEndVnode, newStartVnode)
判斷老節(jié)點(diǎn)的尾節(jié)點(diǎn)和新節(jié)點(diǎn)的初節(jié)點(diǎn)是否是同一類型,如果是贵扰,則對(duì)它們兩個(gè)進(jìn)行patchVnode(patch過程).老節(jié)點(diǎn)的尾節(jié)點(diǎn)向前移動(dòng)一位仇穗,新節(jié)點(diǎn)初節(jié)點(diǎn)向后移動(dòng)一位。
5.如果以上都不滿足戚绕,則創(chuàng)建新的dom節(jié)點(diǎn),newCh的startVnode被添加到oldStartVnode的前面纹坐,同時(shí)newStartIndex后移一位;
用圖來描述就是
遍歷的過程結(jié)束后舞丛,newStartIdx > newEndIdx耘子,說明此時(shí)oldCh存在多余的節(jié)點(diǎn),那么最后就需要將oldCh的多余節(jié)點(diǎn)從parentElm中刪除球切。
如果oldStartIdx > oldEndIdx谷誓,說明此時(shí)newCh存在多余的節(jié)點(diǎn),那么最后就需要將newCh的多余節(jié)點(diǎn)添加到parentElm中吨凑。
diff遍歷的過程如下: (節(jié)點(diǎn)屬性中帶key的情況)
前四步還和上面的一樣
第五步:如果前四步都不滿足捍歪,則首先建立oldCh
key和index索引的對(duì)應(yīng)關(guān)系。
- 如果newStartVnode上存在這個(gè)key,但是oldKeyToIdx中不存在
則創(chuàng)建新的dom節(jié)點(diǎn),newCh的startVnode被添加到oldStartVnode的前面鸵钝,同時(shí)newStartIndex后移一位费封; - 如果找到與
newStartVnode
key一致的oldVnode
則先將這兩個(gè)節(jié)點(diǎn)進(jìn)行patchVnode(patch過程),然后將newStartVnode
移到oldStartVnode
的前面蒋伦,并在oldCh中刪除與newStartVnode
key一致的oldVnode
弓摘,然后新節(jié)點(diǎn)初節(jié)點(diǎn)向后移動(dòng)一位籽慢。再進(jìn)行遍歷硼瓣。
用圖來描述就是
最后,由于newStartIndex>newEndIndex,所以newCh
剩余的節(jié)點(diǎn)會(huì)被添加到parentElm中
總結(jié)
Virtual DOM 算法主要是實(shí)現(xiàn)上面三個(gè)概念:VNode牲证,diff研叫,patch
總結(jié)下來就是
1. 通過構(gòu)造VNode構(gòu)建虛擬DOM
2. 通過虛擬DOM構(gòu)建真正的DOM
3. 生成新的虛擬DOM
4. 比較兩棵虛擬DOM樹的不同.從根節(jié)點(diǎn)開始比較锤窑,diff過程
5. 在真正的DOM元素上應(yīng)用變更,patch
其中patch的過程中遇到兩個(gè)節(jié)點(diǎn)有子節(jié)點(diǎn),則對(duì)其子節(jié)點(diǎn)進(jìn)行diff嚷炉。
而diff的過程又會(huì)調(diào)用patch渊啰。
參考鏈接:
知乎:如何理解虛擬DOM?
Vue原理解析之Virtual Dom
Vue 2.0 的 virtual-dom 實(shí)現(xiàn)簡(jiǎn)析