第二次寫文章,寫得不對的地方望各位大神指正~javascript
以前研究Vue的響應式原理有提到, 當數據發生變化時, Watcher會調用 vm._update(vm._render(), hydrating)
來進行DOM更新, 接下來咱們看看這個具體的更新過程是如何實現的。vue
//摘自core\instance\lifecycle.js
Vue.prototype._update = function(vnode: VNode, hydrating ? : boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */ ,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}複製代碼
( 這裏咱們就將一些不過重要的代碼忽略掉不講了, 好比callHook調用鉤子函數之類的, 咱們只關注實現組件渲染相關代碼。)java
這裏面最重要的代碼就是經過 vm.__patch__
進行DOM更新。 若是以前沒有渲染過, 就直接調用 vm.__patch__
生成真正的DOM並將生成的DOM掛載到vm.$el上, 不然會調用 vm.__patch__(prevVnode, vnode)
將當前vnode與以前的vnode進行diff比較, 最小化更新。node
接下來咱們就看一下這個最重要的 vm.__patch__
到底作了些什麼。web
//摘自platforms\web\runtime\patch.js
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })複製代碼
能夠看到patch方法主要就是調用了createPatchFunction這個函數。 一步步看看它主要乾了些什麼。數組
顧名思義, 這個函數的做用是建立並返回一個patch函數。app
//摘自core\vdom\patch.js
//......
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
//......
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
//......
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}複製代碼
在這個返回的patch函數裏, 會進行許多的判斷:dom
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
建立一個新的DOM。patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
來更新oldVnode並生成新的DOM。( 這裏判斷nodeType是否認義是由於vnode是沒有nodeType的, 當進行服務端渲染時會有nodeType, 這樣能夠排除掉服務端渲染的狀況。 )咱們分別看一下上面的兩種狀況:函數
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */ ,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}複製代碼
若是沒有prevVnode(也就是第一次渲染), 這時vm.$el若是爲undefined則知足 isUndef(oldVnode)
,會調用createElm函數;若是vm.$el存在,但其不知足 sameVnode(oldVnode, vnode)
,一樣會調用createElm函數。也就是說若是是首次渲染,就會調用createElm函數建立新的DOM。post
若是有prevVnode(也就是進行視圖的更新),這時若是知足 sameVnode(oldVnode, vnode)
(即vnode相同),則會調用patchVnode對vnode進行更新;若是vnode不相同,則會調用createElm函數建立新的DOM節點替換掉原來的DOM節點。
那麼接下來分別看看這兩個函數。
//摘自\core\vdom\patch.js
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // for transition enter check
//......
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
//......
createChildren(vnode, children, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
//......
}複製代碼
能夠看到, createElm中主要會根據vnode.ns(vnode的命名空間)是否存在調用createElementNS函數或createElmement函數生成真正的DOM節點並賦給vnode.elm保存。而後經過createChildren函數建立vnode的子節點,而且經過insert函數將vnode.elm插入到父節點中。
//摘自\core\vdom\patch.js
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text))
}
}複製代碼
createChildren函數會判斷vnode的children是不是數組,若是是,則代表vnode有子節點,循環調用createElm函數爲子節點建立DOM;若是是text節點,則會調用createTextNode爲其建立文本節點。
//摘自\core\vdom\patch.js
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
//......
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)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}複製代碼
patchVnode主要是對oldVnode和vnode進行必定的對比:
咱們先來看下比較簡單的當vnode和oldVnode只有其中一個有children時調用的addVnodes和removeVnodes函數。
//摘自\core\vdom\patch.js
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm)
}
}複製代碼
addVnodes函數經過循環調用createElm分別對vnode的children中的每一個子vnode建立子節點並掛載到DOM上。
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}複製代碼
removeVnodes函數經過調用removeNode函數(removeAndInvokeRemoveHook函數最終也是調用removeNode函數)將oldVnode的children節點所有移除。
接下來就看一下當vnode和oldVnode都有children時調用的updateChildren函數。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, 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] : null
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} 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、newCh和parentElm.children。oldCh就是oldVnode.children,newCh就是vnode.children,parentElm就是oldVnode.elm。
而oldStartIdx、oldEndIdx、newStartIdx和newEndIdx這四個是用於標誌當前關注的vnode的頭指針和尾指針。
簡單來講,咱們會將oldCh和newCh進行比較,將oldCh跟newCh差別的部分patch到parentElm中,最終獲得一個根據newCh所對應的elm.children。接下來咱們一步步分析這個函數究竟是如何進行diff的。
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
時繼續進行循環。sameVnode(oldStartVnode, newStartVnode)
,則遞歸調用patchVnode對二者進行比較,同時頭指針往右走。由於咱們最終想要獲得的是newCh所對應的elm,而這個elm是oldVnode.elm,它的children一開始是根據oldCh生成的。那麼當oldStartVnode跟newStartVnode相同時,意味着elm.children中這個位置的子節點已是跟newCh所對應的。sameVnode(oldEndVnode, newEndVnode)
,同理,遞歸調用patchVnode對二者進行比較,同時尾指針往左走。sameVnode(oldStartVnode, newEndVnode)
,意味着newEndVnode跟oldStartVnode相同,這個時候遞歸調用patchVnode對二者進行比較後咱們須要經過nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
,將oldStartVnode.elm移動到parentElm.children中newEndVnode所對應的位置,也就是oldEndVnode.elm後面。sameVnode(oldEndVnode, newStartVnode)
,同理,經過遞歸調用patchVnode對二者進行比較後經過nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
將oldEndVnode.elm移動到parentElm.children中newStartVnode所對應的位置,也就是oldStartVnode.elm前面。createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
建立一個新的DOM節點並插入到oldStartVnode.elm前面。nodeOps.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
將elmToMove.elm移動到oldStartVnode.elm前面。能夠看到,咱們將這個節點設爲了undefined,這樣當指針移動到這裏的時候發現是undefined就會繼續移動,由於這個節點已經被複用了,這個就是上面第2步判斷的做用。oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
時,循環結束。這時候咱們就要判斷究竟是oldStartIdx > oldEndIdx
仍是newStartIdx > newEndIdx
。
oldStartIdx > oldEndIdx
,由於只有當oldCh中的節點被複用時,oldCh的指針纔會移動,當oldCh的頭指針大於尾指針時,意味着oldCh已經沒有節點能夠被複用了,這樣咱們就須要直接將newCh中還未添加到parentElm.children的節點經過addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
添加到parentElm.children中。newStartIdx > newEndIdx
,意味着newCh中的全部節點都已經在parentElm.children中了,也就意味着OldCh中若是oldStartIdx到oldEndIdx之間(包括oldStartIdx和oldEndIdx)指針所指向的節點在newCh中沒有對應的節點,也就是說剩下的都是多餘的節點,因此咱們須要經過removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
將多餘的節點都移除。通過這樣的一個過程以後,parentElm.children就變成了與newCh相對應了。
總的來講,updateChildren的做用是根據newCh生成相應的parentElm.children,同時儘可能複用其中的節點。因此對於每個newCh的節點,會先在oldCh中找相應的節點,找到了就將其移動到parentElm.children中與newCh對應的位置,沒找到就建立一個新的節點插入到對應的位置。最後將parentElm.children中多餘的節點移除或者將newCh中還未添加到parentElm.children中的節點添加上去。
文字描述仍是有點比較難理解,用圖例來進一步解釋。
首先,假設咱們的oldCh有四個節點,用數字表示,分別爲一、二、三、4,newCh五個節點,分別爲五、二、六、三、1。因爲parentElm.children是根據oldCh生成的,因此也有四個節點一、二、三、4。oldCh的頭尾指針分別指向1和4,newCh的頭尾指針分別指向五、1。
parentElm.children | 1 | 2 | 3 | 4 | - |
---|---|---|---|---|---|
oldCh指針 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指針 | ↑ | ↑ |
根據上面咱們說到的updateChildren的判斷過程,判斷到oldCh的頭節點和newCh的尾節點相同,因而就將parentElm.children中的oldCh頭節點移動到oldCh尾節點後面。而後oldCh跟newCh的指針分別移動,因而就變成了下面這樣。
parentElm.children | 2 | 3 | 4 | 1 | - |
---|---|---|---|---|---|
oldCh指針 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指針 | ↑ | ↑ |
繼續進行循環判斷,發現頭尾的節點都沒有相同的,這個時候咱們就要去oldCh中根據key找與newCh頭節點相同的節點。可是沒有找到,因此咱們會建立一個新的節點插入到parentElm.children中頭節點前面,而後指針移動。結果以下。
parentElm.children | 5 | 2 | 3 | 4 | 1 |
---|---|---|---|---|---|
oldCh指針 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指針 | ↑ | ↑ |
繼續進行循環。發現頭節點相同,無需移動,直接對頭節點進行patch,指針移動。結果以下。
parentElm.children | 5 | 2 | 3 | 4 | 1 |
---|---|---|---|---|---|
oldCh指針 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指針 | ↑ | ↑ |
繼續進行循環。發現newCh尾節點和oldCh頭節點相同,將parentElm.children中的3節點移動到parentElm.children的尾指針後面,指針移動。結果以下。
parentElm.children | 5 | 2 | 4 | 3 | 1 |
---|---|---|---|---|---|
oldCh指針 | ↓↓ | ||||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指針 | ↑↑ |
如今兩個頭尾指針都相等了,但仍是符合循環的條件,因而繼續進行循環。因爲兩個節點不相同,因而會建立一個新的節點插入到parentElm.children的頭指針前面,指針移動。結果以下。
parentElm.children | 5 | 2 | 6 | 4 | 3 | 1 |
---|---|---|---|---|---|---|
oldCh指針 | ↓↓ | |||||
oldCh | 1 | 2 | 3 | 4 | ||
newCh | 5 | 2 | 6 | 3 | 1 | |
newCh指針 | ↑ | ↑ |
這樣以後newStartIdx > newEndIdx
,循環結束。由於newStartIdx > newEndIdx
,意味着parentElm.children中可能還有多餘的節點,咱們再調用removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
將多餘的節點移除。結果以下。
parentElm.children | 5 | 2 | 6 | 3 | 1 |
---|---|---|---|---|---|
oldCh指針 | ↓↓ | ||||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指針 | ↑ | ↑ |
這樣,咱們就完成了整一個updateChildren的過程,parentElm.children已經變成了與newCh相對應了。整一個patch的遞歸完成後,vnode.elm就變成全新的elm了,視圖也就更新完畢啦。