示例1:新舊節點不同
<template>
<div id="app">
<hello-world :flag="flag" />
<button @click="toggle">toggle</button>
</div>
</template>
<script> import HelloWorld from './components/HelloWorld/index' export default { name: 'App', components: { HelloWorld }, data() { return { flag: true }; }, methods: { toggle() { this.flag = !this.flag; } } } </script>
<template>
<div class="hello" v-if="flag">
<h1>{{ nested.msg }}</h1>
<h2>Essential links</h2>
<h2>ecosystem</h2>
<ul>
<li><a href="" target="_blank">vur-router</a></li>
<li><a href="" target="_blank">vur-router</a></li>
<li><a href="" target="_blank">vur-router</a></li>
<li><a href="" target="_blank">vur-router</a></li>
<li><a href="" target="_blank">vur-router</a></li>
</ul>
</div>
<ul v-else>
<li><a href="" target="_blank">Twitter</a></li>
<li><a href="" target="_blank">Twitter</a></li>
<li><a href="" target="_blank">Twitter</a></li>
<li><a href="" target="_blank">Twitter</a></li>
<li><a href="" target="_blank">Twitter</a></li>
</ul>
</template>
<script> export default { name: "index", props: ['flag'], data() { return { nested: { mag: 'welcome to your vue.js app' } }; } } </script>
-
當點擊toggle時,首先觸發setter,然后走到App組件的渲染watcher update,在nextTick中執行flushSchedulerQueue(標記 flushing 為 true ),執行watcher.run,執行get。
-
走到App組件的patch,會走到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) ) ) ) }
-
此時判斷新舊節點相同,進入patchVnode,當更新的vnode是一個組件vnode的時候,會執行prepatch方法,此時會執子組件(HelloWorld組件)的prepatch鈎子函數
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) }
-
該函數執行updateChildComponent,該函數對子組件數據做更新(對$attrs,$listeners,props等),更新flag的變化時,觸發dep.notify(),執行子組件的update,此時由於flushing會true,渲染watcher會進入當前queue,所以父子組件的更新在一個tick中。
-
進入到子組件的ptach,此時新舊節點不同,會進入新舊節點不同的邏輯,首先創建vode,然后執行更新父組件的占位符vnode,首先判斷當前vnode是否可掛載,最后移除舊節點,插入新節點。
示例2:新舊節點相同且都有子節點
<template>
<div id="app">
<div>
<ul>
<li v-for="item in items" :key="item.id">{{ item.val }}</li>
</ul>
</div>
<button @click="change">change</button>
</div>
</template>
<script>
// import HelloWorld from './components/HelloWorld/index'
export default { name: 'App', components: { // HelloWorld
}, data() { return { items: [ { id: 0, val: 'A'}, { id: 1, val: 'B'}, { id: 2, val: 'C'}, { id: 3, val: 'D'}, ] }; }, methods: { change() { this.items.reverse().push({ id: 4, val: 'E'}); } } } </script>
- 點擊change按鈕進入App組件的patch過程,此時新舊節點不同且都存在子節點進入patchVnode,分別拿新舊節點的child,進入updateChildren,此函數是組件更新核心也是最復雜的
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, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] 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, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } }
- 該函數的核心是執行新舊vnode的diff算法,根據不同情況做不同的更新邏輯,遞歸調用patchVnode方法,最后執行完之后頁面已經被替換成了新節點。
總結:
1、組件更新的過程核心就是新舊vnode diff算法,對新舊節點相同以及不同的情況分別做不同的處理。
2、新舊節點不同的更新流程是:創建新節點=>更新父組件占位符=>刪除舊節點。
3、新舊節點相同的更新流程是去獲取它們的child,根據不同情況做不同的更新邏輯。
