初六和家人出去玩,沒寫完博客。跳票了~
所謂虛擬DOM,是一個(gè)用于表示真實(shí) DOM 結(jié)構(gòu)和屬性的 JavaScript 對象,這個(gè)對象用于對比虛擬 DOM 和當(dāng)前真實(shí) DOM 的差異化,然后進(jìn)行局部渲染從而實(shí)現(xiàn)性能上的優(yōu)化。在Vue.js 中虛擬 DOM 的 JavaScript 對象就是 VNode。
接下來我們一步步分析:
VNode 是什么?
既然是虛擬 DOM 的作用是轉(zhuǎn)為真實(shí)的 DOM,那這就是一個(gè)渲染的過程。所以我們看看 render 方法。在之前的學(xué)習(xí)中我們知道了,vue 的渲染函數(shù) _render 方法返回的就是一個(gè) VNode 對象。而在 initRender 初始化渲染的方法中定義的 vm._c 和 vm.$createElement 方法中,createElement 最終也是返回 VNode 對象。所以 VNode 是渲染的關(guān)鍵所在。
話不多說,來看看這個(gè)VNode為何方神圣。
// src/core/vdom/vnode.js
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;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
fnScopeId: ?string; // functioanl scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag // 當(dāng)前節(jié)點(diǎn)標(biāo)簽名
this.data = data // 當(dāng)前節(jié)點(diǎn)數(shù)據(jù)(VNodeData類型)
this.children = children // 當(dāng)前節(jié)點(diǎn)子節(jié)點(diǎn)
this.text = text // 當(dāng)前節(jié)點(diǎn)文本
this.elm = elm // 當(dāng)前節(jié)點(diǎn)對應(yīng)的真實(shí)DOM節(jié)點(diǎn)
this.ns = undefined // 當(dāng)前節(jié)點(diǎn)命名空間
this.context = context // 當(dāng)前節(jié)點(diǎn)上下文
this.fnContext = undefined // 函數(shù)化組件上下文
this.fnOptions = undefined // 函數(shù)化組件配置項(xiàng)
this.fnScopeId = undefined // 函數(shù)化組件ScopeId
this.key = data && data.key // 子節(jié)點(diǎn)key屬性
this.componentOptions = componentOptions // 組件配置項(xiàng)
this.componentInstance = undefined // 組件實(shí)例
this.parent = undefined // 當(dāng)前節(jié)點(diǎn)父節(jié)點(diǎn)
this.raw = false // 是否為原生HTML或只是普通文本
this.isStatic = false // 靜態(tài)節(jié)點(diǎn)標(biāo)志 keep-alive
this.isRootInsert = true // 是否作為根節(jié)點(diǎn)插入
this.isComment = false // 是否為注釋節(jié)點(diǎn)
this.isCloned = false // 是否為克隆節(jié)點(diǎn)
this.isOnce = false // 是否為v-once節(jié)點(diǎn)
this.asyncFactory = asyncFactory // 異步工廠方法
this.asyncMeta = undefined // 異步Meta
this.isAsyncPlaceholder = false // 是否為異步占位
}
// 容器實(shí)例向后兼容的別名
get child (): Component | void {
return this.componentInstance
}
}
其實(shí)就是一個(gè)普通的 JavaScript Class 類,中間有各種數(shù)據(jù)用于描述虛擬 DOM,下面用一個(gè)例子來看看VNode 是如何表現(xiàn) DOM 的。
<div id="app">
<span>{{ message }}</span>
<ul>
<li v-for="item of list" class="item-cls">{{ item }}</li>
</ul>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'hello Vue.js',
list: ['jack', 'rose', 'james']
}
})
</script>
這個(gè)例子最終結(jié)果如圖:
簡化后的VNode對象結(jié)果如圖:
{
"tag": "div",
"data": {
"attr": { "id": "app" }
},
"children": [
{
"tag": "span",
"children": [
{ "text": "hello Vue.js" }
]
},
{
"tag": "ul",
"children": [
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "jack" }
]
},
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "rose" }
]
},
{
"tag": "li",
"data": { "staticClass": "item-cls" },
"children": [
{ "text": "james" }
]
}
]
}
],
"context": "$Vue$3",
"elm": "div#app"
}
在看VNode的時(shí)候小結(jié)以下幾點(diǎn):
- 所有對象的
context選項(xiàng)都指向了 Vue 實(shí)例。 -
elm屬性則指向了其相對應(yīng)的真實(shí) DOM 節(jié)點(diǎn)。 - DOM 中的文本內(nèi)容被當(dāng)做了一個(gè)只有
text沒有tag的節(jié)點(diǎn)。 - 像 class、id 等HTML屬性都放在了
data中
我們了解了VNode 是如何描述 DOM 之后,來學(xué)習(xí)如何將虛擬
DOM 變?yōu)檎鎸?shí)的 DOM。
patch —— Virtual DOM 的核心
從之前的文章中可以知道,Vue的渲染過程(無論是初始化視圖還是更新視圖)最終都將走到 _update 方法中,再來看看這個(gè) _update 方法。
// src/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) {
// 初始化渲染
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// 更新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
不難發(fā)現(xiàn)更新試圖都是使用了 vm.__patch__ 方法,我們繼續(xù)往下跟。
// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
這里啰嗦一句,要找vue的全局方法,如 vm.aaa ,直接查找 Vue.prototype.aaa 即可。
繼續(xù)找下去:
// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
找到 createPatchFunction 方法~
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
……
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 當(dāng)前 VNode 未定義、老的 VNode 定義了,調(diào)用銷毀鉤子。
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 老的 VNode 未定義,初始化。
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
// 當(dāng)前 VNode 和老 VNode 都定義了,執(zhí)行更新操作
// DOM 的 nodeType http://www.w3school.com.cn/jsref/prop_node_nodetype.asp
const isRealElement = isDef(oldVnode.nodeType) // 是否為真實(shí) DOM 元素
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 修改已有根節(jié)點(diǎn)
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
// 已有真實(shí) DOM 元素,處理 oldVnode
if (isRealElement) {
// 掛載一個(gè)真實(shí)元素,確認(rèn)是否為服務(wù)器渲染環(huán)境或者是否可以執(zhí)行成功的合并到真實(shí) DOM 中
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// 調(diào)用 insert 鉤子
// inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
// 不是服務(wù)器渲染或者合并到真實(shí) DOM 失敗,創(chuàng)建一個(gè)空節(jié)點(diǎn)替換原有節(jié)點(diǎn)
oldVnode = emptyNodeAt(oldVnode)
}
// 替換已有元素
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 創(chuàng)建新節(jié)點(diǎn)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 遞歸更新父級占位節(jié)點(diǎn)元素,
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 銷毀舊節(jié)點(diǎn)
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 調(diào)用 insert 鉤子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
具體解析看代碼注釋~拋開調(diào)用生命周期鉤子和銷毀就節(jié)點(diǎn)不談,我們發(fā)現(xiàn)代碼中的關(guān)鍵在于 createElm 和 patchVnode 方法。
createElm
先看 createElm 方法,這個(gè)方法創(chuàng)建了真實(shí) DOM 元素。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 創(chuàng)建組件
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
重點(diǎn)關(guān)注代碼中的方法執(zhí)行。代碼太多,就不貼出來了,簡單說說用途。
-
cloneVNode用于克隆當(dāng)前 vnode 對象。 -
createComponent用于創(chuàng)建組件,在調(diào)用了組件初始化鉤子之后,初始化組件,并且重新激活組件。在重新激活組件中使用insert方法操作 DOM。 -
nodeOps.createElementNS和nodeOps.createElement方法,其實(shí)是真實(shí) DOM 的方法。 -
setScope用于為 scoped CSS 設(shè)置作用域 ID 屬性 -
createChildren用于創(chuàng)建子節(jié)點(diǎn),如果子節(jié)點(diǎn)是數(shù)組,則遍歷執(zhí)行createElm方法,如果子節(jié)點(diǎn)的 text 屬性有數(shù)據(jù),則使用nodeOps.appendChild(...)在真實(shí) DOM 中插入文本內(nèi)容。 -
insert用于將元素插入真實(shí) DOM 中。
所以,這里的 nodeOps 指的肯定就是真實(shí)的 DOM 節(jié)點(diǎn)了。最終,這些所有的方法都調(diào)用了 nodeOps 中的方法來操作 DOM 元素。
這里順便科普下 DOM 的屬性和方法。下面把源碼中用到的幾個(gè)方法列出來便于學(xué)習(xí):
- appendChild: 向元素添加新的子節(jié)點(diǎn),作為最后一個(gè)子節(jié)點(diǎn)。
- insertBefore: 在指定的已有的子節(jié)點(diǎn)之前插入新節(jié)點(diǎn)。
- tagName: 返回元素的標(biāo)簽名。
- removeChild: 從元素中移除子節(jié)點(diǎn)。
- createElementNS: 創(chuàng)建帶有指定命名空間的元素節(jié)點(diǎn)。
- createElement: 創(chuàng)建元素節(jié)點(diǎn)。
- createComment: 創(chuàng)建注釋節(jié)點(diǎn)。
- createTextNode: 創(chuàng)建文本節(jié)點(diǎn)。
- setAttribute: 把指定屬性設(shè)置或更改為指定值。
- nextSibling: 返回位于相同節(jié)點(diǎn)樹層級的下一個(gè)節(jié)點(diǎn)。
- parentNode: 返回元素父節(jié)點(diǎn)。
- setTextContent: 獲取文本內(nèi)容(這個(gè)未在w3school中找到,不過應(yīng)該就是這個(gè)意思了)。
OK,知道以上方法就比較好理解了,createElm 方法的最終目的就是創(chuàng)建真實(shí)的 DOM 對象。
patchVnode
看過了創(chuàng)建真實(shí) DOM 后,我們來學(xué)習(xí)虛擬 DOM 如何實(shí)現(xiàn) DOM 的更新。這才是虛擬 DOM 的存在意義 —— 比對并局部更新 DOM 以達(dá)到性能優(yōu)化的目的。
看代碼~
// 補(bǔ)丁 vnode
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 新舊 vnode 相等
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// 異步占位
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新舊 vnode 為靜態(tài);新舊 vnode key相同;
// 新 vnode 是克隆所得;新 vnode 有 v-once 的屬性
// 則新 vnode 的 componentInstance 用老的 vnode 的。
// 即 vnode 的 componentInstance 保持不變。
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 執(zhí)行 data.hook.prepatch 鉤子。
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 遍歷 cbs,執(zhí)行 update 方法
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 執(zhí)行 data.hook.update 鉤子
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 舊 vnode 的 text 選項(xiàng)為 undefined
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新舊 vnode 都有 children,且不同,執(zhí)行 updateChildren 方法。
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 清空文本,添加 vnode
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 移除 vnode
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果新舊 vnode 都是 undefined,清空文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 有不同文本內(nèi)容,更新文本內(nèi)容
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 執(zhí)行 data.hook.postpatch 鉤子,表明 patch 完畢
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
源碼中添加了一些注釋便于理解,來理一下邏輯。
- 如果兩個(gè)vnode相等,不需要 patch,退出。
- 如果是異步占位,執(zhí)行 hydrate 方法或者定義 isAsyncPlaceholder 為 true,然后退出。
- 如果兩個(gè)vnode都為靜態(tài),不用更新,所以講以前的 componentInstance 實(shí)例傳給當(dāng)前 vnode,并退出。
- 執(zhí)行 prepatch 鉤子。
- 遍歷調(diào)用 update 回調(diào),并執(zhí)行 update 鉤子。
- 如果兩個(gè) vnode 都有 children,且 vnode 沒有 text、兩個(gè) vnode 不相等,執(zhí)行 updateChildren 方法。這是虛擬 DOM 的關(guān)鍵。
- 如果新 vnode 有 children,而老的沒有,清空文本,并添加 vnode 節(jié)點(diǎn)。
- 如果老 vnode 有 children,而新的沒喲,清空文本,并移除 vnode 節(jié)點(diǎn)。
- 如果兩個(gè) vnode 都沒有 children,老 vnode 有 text ,新 vnode 沒有 text ,則清空 DOM 文本內(nèi)容。
- 如果老 vnode 和新 vnode 的 text 不同,更新 DOM 元素文本內(nèi)容。
- 調(diào)用 postpatch 鉤子。
其中,addVnodes 方法和 removeVnodes 都比較簡單,很好理解。這里我們來看看關(guān)鍵代碼 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 是一個(gè)只用于 <transition-group> 的特殊標(biāo)簽,
// 確保移除元素過程中保持一個(gè)正確的相對位置。
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 開始老 vnode 向右一位
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 結(jié)束老 vnode 向左一位
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 新舊開始 vnode 相似,進(jìn)行pacth。開始 vnode 向右一位
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 新舊結(jié)束 vnode 相似,進(jìn)行patch。結(jié)束 vnode 向左一位
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 新結(jié)束 vnode 和老開始 vnode 相似,進(jìn)行patch。
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 老開始 vnode 插入到真實(shí) DOM 中,老開始 vnode 向右一位,新結(jié)束 vnode 向左一位
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 老結(jié)束 vnode 和新開始 vnode 相似,進(jìn)行 patch。
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 老結(jié)束 vnode 插入到真實(shí) DOM 中,老結(jié)束 vnode 向左一位,新開始 vnode 向右一位
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 獲取老 Idx 的 key
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 給老 idx 賦值
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 如果老 idx 為 undefined,說明沒有這個(gè)元素,創(chuàng)建新 DOM 元素。
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 獲取 vnode
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 如果生成的 vnode 和新開始 vnode 相似,執(zhí)行 patch。
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 賦值 undefined,插入 vnodeToMove 元素
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 相同的key不同的元素,視為新元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 新開始 vnode 向右一位
newStartVnode = newCh[++newStartIdx]
}
}
// 如果老開始 idx 大于老結(jié)束 idx,如果是有效數(shù)據(jù)則添加 vnode 到新 vnode 中。
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 移除 vnode
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
表示已看暈……讓我們慢慢捋一捋……
- 看參數(shù),其中 oldCh 和 newCh 即表示了新舊 vnode 數(shù)組,兩組數(shù)組通過比對的方式來差異化更新 DOM。
- 定義了一些變量:開始索引值、結(jié)束索引值、開始vnode、結(jié)束vnode等等……
- 進(jìn)行循環(huán)遍歷,遍歷條件為 oldStartIdx <= oldEndIdx 和 newStartIdx <= newEndIdx,在遍歷過程中,oldStartIdx 和 newStartIdx 遞增,oldEndIdx 和 newEndIdx 遞減。當(dāng)條件不符合跳出遍歷循環(huán)。
-
如果 oldStartVnode 和 newStartVnode 相似,執(zhí)行 patch。
image - 如果 oldEndVnode 和 newEndVnode 相似,執(zhí)行 patch。
-
如果 oldStartVnode 和 newEndVnode 相似,執(zhí)行 patch,并且將該節(jié)點(diǎn)移動到 vnode 數(shù)組末一位。
image -
如果 oldEndVnode 和 newStartVnode 相似,執(zhí)行 patch,并且將該節(jié)點(diǎn)移動到 vnode 數(shù)組第一位。
image - 如果沒有相同的 idx,執(zhí)行 createElm 方法創(chuàng)建元素。
-
如果如有相同的 idx,如果兩個(gè) vnode 相似,執(zhí)行 patch,并且將該節(jié)點(diǎn)移動到 vnode 數(shù)組第一位。如果兩個(gè) vnode 不相似,視為新元素,執(zhí)行 createElm 創(chuàng)建。
image -
如果老 vnode 數(shù)組的開始索引大于結(jié)束索引,說明新 node 數(shù)組長度大于老 vnode 數(shù)組,執(zhí)行 addVnodes 方法添加這些新 vnode 到 DOM 中。
image -
如果老 vnode 數(shù)組的開始索引小于結(jié)束索引,說明老 node 數(shù)組長度大于新 vnode 數(shù)組,執(zhí)行 removeVnodes 方法從 DOM 中移除老 vnode 數(shù)組中多余的 vnode。
image
嗯……就是這樣!
最后
畢竟是Vue的核心功能之一,雖然省略了不少代碼,但博客篇幅很長。寫了兩天才寫完。不過寫完博客后感覺對于 Vue 的理解又加深了很多。
在下一篇博客中,我們一起來學(xué)習(xí)template的解析。
參考文檔
Vue.js學(xué)習(xí)系列
鑒于前端知識碎片化嚴(yán)重,我希望能夠系統(tǒng)化的整理出一套關(guān)于Vue的學(xué)習(xí)系列博客。
Vue.js學(xué)習(xí)系列項(xiàng)目地址
本文源碼已收入到GitHub中,以供參考,當(dāng)然能留下一個(gè)star更好啦-。
https://github.com/violetjack/VueStudyDemos
關(guān)于作者
VioletJack,高效學(xué)習(xí)前端工程師,喜歡研究提高效率的方法,也專注于Vue前端相關(guān)知識的學(xué)習(xí)、整理。
歡迎關(guān)注、點(diǎn)贊、評論留言~我將持續(xù)產(chǎn)出Vue相關(guān)優(yōu)質(zhì)內(nèi)容。
新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571d953d39b0570068145cd1
CSDN: http://blog.csdn.net/violetjack0808
簡書: http://www.itdecent.cn/users/54ae4af3a98d/latest_articles
Github: https://github.com/violetjack