建議PC端觀看,移動端代碼高亮錯亂html
在組件化章節,咱們介紹了 Vue
的組件化實現過程,不過咱們只講了 Vue
組件的建立過程,並無涉及到組件數據發生變化,更新組件的過程。vue
而經過咱們這一章對數據響應式原理的分析,瞭解到當數據發生變化的時候,會觸發 渲染watcher
的回調函數,進而執行組件的更新過程。node
接下來咱們來詳細分析這一過程。web
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製代碼
組件的更新仍是調用了 vm._update
方法,咱們再回顧一下這個方法,它的定義在 src/core/instance/lifecycle.js
中:bash
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// ...
const prevVnode = vm._vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}
複製代碼
組件更新的過程,會執行 vm.$el = vm.__patch__(prevVnode, vnode)
,它仍然會調用 patch
函數,在 src/core/vdom/patch.js
中定義:dom
vm.__patch__(prevVnode, vnode)
function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// ...
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 新舊vnode相同的狀況
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 新舊vnode不一樣的狀況
// 1. 建立新節點
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
parentElm,
nodeOps.nextSibling(oldElm)
)
// 2. 遞歸更新父的佔位符節點
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
// 遍歷 cbs.destroy,依次destroy執行鉤子...
ancestor.elm = vnode.elm
// 是否可掛載
if (patchable) {
// 遍歷cbs.create,依次執行create鉤子...
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 刪除舊節點
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// ...
return vnode.elm
}
複製代碼
這裏執行 patch
的邏輯和首次渲染是不同的,由於 oldVnode
不爲空,而且它和 vnode
都是 VNode
類型,接下來會經過 sameVNode(oldVnode, vnode)
判斷它們是不是相同的 VNode
來決定走不一樣的更新邏輯:異步
// src/core/vdom/patch.js
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)
)
)
)
}
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)
}
複製代碼
vnode
的 key
不相等,則確定是不一樣的isComment
、data
、input
類型等是否相同asyncFactory
是否相同。因此根據新舊 vnode
是否爲 sameVnode
,會走到不一樣的更新邏輯,咱們先來講一下不一樣的狀況。async
其實在平常開發中基本上不會走到這個邏輯,只有當咱們這麼編寫組件時:編輯器
<template>
<div v-if="flag">
</div>
<ul v-else>
<li>1</li>
<li>2</li>
</ul>
</template>
<script>
export default {
data() {
return {
flag: true
}
}
};
</script>
複製代碼
因爲咱們在最外層節點用了 v-if
,因此會產生新舊節點不一樣的狀況,可是一般咱們都是在最外層用一個標籤包裹的。函數
下面回到 patch
函數,來看看新舊節點不一樣的狀況,這部分邏輯分爲三部分:
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
parentElm,
nodeOps.nextSibling(oldElm)
)
複製代碼
vnode
的 DOM
爲基礎得到父節點。createElm
:經過舊 vnode
建立真實的 DOM
並插入到它的父節點中。if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
// 遍歷 cbs.destroy,依次destroy執行鉤子...
ancestor.elm = vnode.elm
// 是否可掛載
if (patchable) {
// 遍歷cbs.create,依次執行create鉤子...
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
複製代碼
咱們只關注主要邏輯便可:獲取當前 渲染vnode
的 父佔位符vnode
(若是存在的話),先執行各個 module
的 destroy
的鉤子函數,若是當前佔位符是一個可掛載的節點,則執行 module
的 create
鉤子函數。對於這些鉤子函數的做用,在以後的章節會詳細介紹。
一般狀況下 while
循環只執行一次:
// parent.vue
<template>
<div>
parent
<Child></Child>
</div>
</template>
// child.vue
<template>
<div>child</div>
</template>
複製代碼
只有如下這種狀況 while
循環會屢次執行
// parent.vue
<template>
<Child></Child>
</template>
// child.vue
<template>
<div>child</div>
</template>
複製代碼
也就是說 父佔位符vnode
同時又是一個 渲染vnode
的狀況。
經過 isPatchable
函數用於判斷是否可掛載,源碼以下:
function isPatchable (vnode) {
// 存在 componentInstance 表示當前的渲染vnode,同時也是另外一個組件的佔位符vnode
// 這種狀況則循環,直到找到最深層的組件
while (vnode.componentInstance) {
vnode = vnode.componentInstance._vnode
}
return isDef(vnode.tag)
}
複製代碼
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
複製代碼
把 oldVnode
從當前 DOM
樹中刪除,這其中執行 beforeDestroy & destroyed
兩個生命週期鉤子。
在 patch
函數中,當新舊 vnode
相同時:
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
複製代碼
執行了 patchVnode
方法,參數咱們只關注 oldVnode & vnode
便可。
// src/core/vdom/patch.js
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// ...
// 執行 prepatch 鉤子函數
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 執行 update 鉤子函數
if (isDef(data) && isPatchable(vnode)) {
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)
}
// patch 過程
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 (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
// 執行 postpatch 鉤子函數
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
複製代碼
patchVnode
函數的主要作了這幾件事:
prepatch
鉤子函數,這部分在下一章再結合 props
展開分析module
的 update
鉤子函數以及用戶自定義的 update
鉤子函數patch
過程postpatch
鉤子函數咱們本章重點來關注核心的 patch
過程
// patch 過程
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 (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
複製代碼
若是 vnode
不是文本節點,則判斷它們的子節點,並分了幾種狀況處理:
oldCh
與 ch
都存在且不相同時,使用 updateChildren
函數來更新子節點,這個稍後重點講。ch
存在,表示舊節點不須要了。若是舊的節點是文本節點則先將節點的文本清除,而後經過 addVnodes
將 ch
批量插入到新節點 elm
下。oldCh
存在,表示更新的是空節點,則須要將舊的節點經過 removeVnodes
所有清除。不然 vnode
是個文本節點且新舊文本不相同時,直接替換文本內容。
先貼出完整代碼,再結合圖片一步一步來看。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newStartIdx = 0
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]
} 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)
}
}
複製代碼
開始以前定義了一系列的變量,分別以下:
oldStartIdx
:oldCh
的開始指針,對應的vnode
是 oldStartVnode
oldEndIdx
:oldCh
的結束指針,對應的 vnode
是 oldEndVnode
newStartIdx
:ch
的開始指針,對應的 vnode
是 newStartVnode
newEndIdx
:ch
的結束指針,對應的 vnode
是 newEndVnode
oldKeyToIdx
是一個 map
,其中 key
就是常在 for
循環中寫的 key
的值,value
就是當前 vnode
,也就是能夠經過惟一的 key
,在 map
中找到對應的 vnode
接下來是一個 while
循環,在這過程當中,oldStartIdx
、newStartIdx
、oldEndIdx
以及 newEndIdx
會逐漸向中間靠攏。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
複製代碼
首先當 oldStartVnode
或者 oldEndVnode
不存在的時候,oldStartIdx
與 oldEndIdx
繼續向中間靠攏,並更新對應的 oldStartVnode
與 oldEndVnode
的指向
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
複製代碼
接下來是將 oldStartVode
、newStartVode
、oldEndVode
以及 newEndVode
兩兩比對的過程,一共會出現 2*2=4 種狀況。
首先是 oldStartVnode
與 newStartVnode
符合 sameVnode
時,直接進行 patchVnode
,同時 oldStartIdx
與 newStartIdx
向後移動一位。
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
複製代碼
其次是 oldEndVnode
與 newEndVnode
符合 sameVnode
,一樣進行 patchVnode
操做並將 oldEndVnode
與 newEndVnode
向前移動一位。
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
複製代碼
先是 oldStartVnode
與 newEndVnode
符合 sameVnode
的時候,將 oldStartVnode.elm
這個節點直接移動到 oldEndVnode.elm
這個節點的後面便可。而後 oldStartIdx
向後移動一位,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]
}
複製代碼
同理,oldEndVnode
與 newStartVnode
符合 sameVnode
時,將 oldEndVnode.elm
插入到 oldStartVnode.elm
前面。一樣的,oldEndIdx
向前移動一位,newStartIdx
向後移動一位。
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]
}
複製代碼
經過 createKeyToOldIdx
產生 key
與 index
索引對應的一個 map 表:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
複製代碼
好比說:
[
{xx: xx, key: 'key0'},
{xx: xx, key: 'key1'},
{xx: xx, key: 'key2'}
]
複製代碼
在通過 createKeyToOldIdx
轉化之後會變成:
{
key0: 0,
key1: 1,
key2: 2
}
複製代碼
咱們能夠根據某一個 key
的值,快速地從 oldKeyToIdx
這個 map
中獲取相同 key
的節點的索引 idxInOld
,而後找到相同的節點。
若是沒有 key
值則調用 findIdxInOld
從 oldCh
找到相同 vnode
,findIdxInOld
函數定義以下:
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
}
}
複製代碼
若是沒有找到相同的節點,則經過 createElm
建立一個新節點,並將 newStartIdx
向後移動一位。
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
複製代碼
不然若是找到了節點,同時它符合 sameVnode
,則將這兩個節點進行 patchVnode
,將該位置的老節點賦值 undefined
(以後若是還有新節點與該節點key相同能夠檢測出來提示已有重複的 key ),同時將 vnodeToMove.elm
插入到 oldStartVnode.elm
的前面。同理,newStartIdx
日後移動一位。
若是不符合 sameVnode
,只能建立一個新節點插入到 parentElm
的子節點中,newStartIdx
日後移動一位。
最後一步就很容易啦,當 while
循環結束之後,若是 oldStartIdx > oldEndIdx
,說明老節點比對完了,可是新節點還有多的,須要將新節點插入到真實 DOM
中去,調用 addVnodes
將這些節點插入便可。
同理,若是知足 newStartIdx > newEndIdx
條件,說明新節點比對完了,老節點還有多,將這些無用的老節點經過 removeVnodes
批量刪除便可。