highlight: tomorrow-night-eighties
theme: cyanosis
前言
Vue 的 patch 过程分为上下两篇
通过这篇文章可以了解如下内容
- Vue 更新过程
- Vue 的 diff 算法
- diff 算法的时间复杂度
v-for
中key
的作用
更新过程
修改响应式属性时,会通知订阅Watcher更新,从而触发组件重新渲染;首先还是执行组件render
函数获取组件VNode,然后执行_update
javascript Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el // dom 节点 // 获取更新前的VNode const prevVnode = vm._vnode // 设置 activeInstance 并返回一个匿名函数,匿名函数返回值是上一个 activeInstance 的值 const restoreActiveInstance = setActiveInstance(vm) // 当前 Vue 实例的 render 函数创建的 VNode vm._vnode = vnode if (!prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode) } // 将 activeInstance 的值设置成上一个 vm 实例 restoreActiveInstance() // update __vue__ reference 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 } }
相比于初次渲染,更新过程中的prevVnode
是有值的(如果是通过v-if
控制prevVnode
没有值),值为更新前的VNode;所以会走else
逻辑,else
逻辑也是调用vm.__patch__
函数,但是会传入prevVnode
。注意这里会将vm._vnode
设置成最新的VNode;
接着看patch
函数
```javascript return function patch (oldVnode, vnode, hydrating, removeOnly) { // 新节点不存在,老节点存在,销毁老节点 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 更新 组件vnode 的 elm 并重新执行父组件的 cbs.create 和 insert hooks(不包含 mounted 钩子)
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)
}
// 更新 组件vnode 的 elm
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) {
// 从 1 开始,因为第一个insert hook 是 mounted
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
} ```
更新过程中,patch
函数会出现3种情况,分别是:
- 当前组件第一次创建,比如父组件通过
v-if
控制子组件是否渲染 - 新老节点相同
- 新老节点不同
至于第一种情况,和初次渲染流程相同,这里就不多赘述了。下面这种情况会走第一种逻辑 xml <template> <div> <cmp1 v-if="xxx">xxx</cmp1> <cmp2 v-else>yyy</cmp2> </div> </template>
当修改xxx
为false
,会创建cmp2
,此时 cmp2
的 oldVNode
为 null
。也就是说如果组件在初次渲染挂载过,在更新阶段就有oldVNode
,反之没有
而第二三种情况根据下面的逻辑判断
javascript // patch 函数内部 // oldVnode 不是真实节点,并且 sameVnode 返回 true if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) }
sameVnode
函数
javascript 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) ) ) ) }
- 对于同步组件,如果两个
vnode
的key
不相等,则不同;如果key
相同则继续判断isComment
、data
、input
类型等是否相同 - 对于异步组件,如果两个
vnode
的key
不相等,则不同;如果key
相同则继续判断asyncFactory
是否相同
新老节点相同
当!isRealElement && sameVnode(oldVnode, vnode)
成立,会执行patchVnode
函数
```javascript function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { if (oldVnode === vnode) { return } // 将 oldVnode.elm 赋值给 vnode.elm 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 } 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 // 这里需要注意:组件占位符 VNode 的 Vnode.children 属性始终为空 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)) { for (i = 0; i < cbs.update.length; ++i) cbs.updatei if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } 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) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } } ```
patchVnode
的作用就是 更新VNode的elm
属性,也就是说接下来的过程其实就是修改DOM树的过程
他的逻辑是,如果新老节点的 VNode 相同则返回。反之,将 oldVnode.elm
赋值给 vnode.elm
;如果VNode是组件占位符VNode,执行VNode的prepatch
钩子函数去更新子组件;然后,获取新老节点的子节点;执行cbs.update
里面的所有函数和VNode的update
钩子函数,全量更新节点的所有属性;然后开始比对,如果新节点是文本节点且新旧文本不相同,则直接替换elm
文本内容。如果新VNode不是文本节点,则判断它们的子节点,并分了几种情况处理:
- 新老节点都有子节点,并且子节点不相同,使用
updateChildren
函数来更新子节点 - 如果只有新VNode有子节点,说明老节点要么是文本节点要么就是没有子节点;如果旧的节点是文本节点将节点的文本清除;然后通过
addVnodes
将新VNode的所有子节点批量插入到新节点elm
下 - 如果只有老VNode有子节点,说明新VNode是空节点;则将老VNode的所有子节点通过
removeVnodes
全部清除 - 当只有旧节点是文本节点的时候,则清除其节点文本内容
上述执行完之后,会执行postpatch
钩子函数
updateChildren
上面第一条中当新老节点都有子节点,并且子节点不相同时会调用updateChildren
函数;看下updateChildren
函数是怎么更新子节点的,其实这个就是一个递归过程,代码如下(可以直接看后面的解释部分就行)
```javascript 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
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } 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, 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 { 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) } } ```
首先会定义4个指针以及4个指针对应的VNode节点
oldStartIdx
、oldEndIdx
、newStartIdx
、newEndIdx
分别是新老两个VNode的两边索引oldStartVnode
、oldEndVnode
、newStartVnode
、newEndVnode
分别指向这几个索引对应的VNode节点
然后是一个while
循环,oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
;在这个过程中4个指针会逐渐向中间靠拢,直到老节点的开始索引大于老节点的结束索引或者新节点的开始索引大于新节点的结束索引时,while
循环结束。
while
循环内的逻辑如下 1. oldStartVnode
没有的情况,oldStartIdx
向中间靠拢,并更新oldStartVnode
的值 2. oldEndVnode
没有的情况,oldEndIdx
向中间靠拢,并更新oldEndVnode
的值 3. oldStartVnode
和 newStartVnode
是相同节点,也就是两个节点的开头是相同的,调用patchVnode
去更新子节点,子节点更新完成之后,将 oldStartIdx
与 newStartIdx
向后移动一位 4. oldEndVnode
和 newEndVnode
是相同节点,也就是两个节点的结尾是相同的,同样进行 patchVnode
操作并将 oldEndIdx
与 newEndVnode
向前移动一位 5. oldStartVnode
和 newEndVnode
是相同节点,也就是老节点的头部与新节点的尾部是同一节点时,调用patchVnode
,更新子节点;子节点更新完成之后,将 oldStartVnode.elm
移动到 oldEndVnode.elm
后面;然后 oldStartIdx
向后移动一位,newEndIdx
向前移动一位。
oldEndVnode
和newStartVnode
是相同节点,也就是老节点的尾部与新节点的头部是同一节点的时候,调用patchVnode
,更新子节点;子节点更新完成之后,将oldEndVnode.elm
插入到oldStartVnode.elm
前面;oldEndIdx
向前移动一位,newStartIdx
向后移动一位。
- 如果上述都没命中,进入下面的逻辑
javascript else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { 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 { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] }
首先通过createKeyToOldIdx
获取oldCh
中所有key
,{ [key的名字]: [在oldCh中的索引] }
;如果newStartVnode
有key
,获取这个 key
在oldCh
中的位置,并赋值给idxInOld
;否则,遍历 oldCh
,查找和 newStartVnode
相同的节点,如果找到了就返回对应的索引,并赋值给idxInOld
。接下来逻辑如下:
如果
idxInOld
为undefined
,说明newStartVnode
和oldCh
中所有节点都不相同,调用createElm
创建节点,并插入到oldStartVnode.elm
前面。让newStartIdx
往后一位,并更新newStartVnode
的值如果
idxInOld
有值,说明newStartVnode
在oldCh
中有一样的节点或者相同节点,获取这个节点,并再次通过sameVnode
判断这个节点和newStartVnode
是否相同- 如果相同调用
patchVnode
更新子节点,子节点更新完成后将这个节点从oldCh
中删除,并将vnodeToMove.elm
(oldCh[key].elm
)插到oldStartVnode.elm
前面;让newStartIdx
往后一位,并更新newStartVnode
的值 - 如果不同,调用
createElm
创建节点,并插入到oldStartVnode.elm
前面。让newStartIdx
往后一位,并更新newStartVnode
的值
- 如果相同调用
当while
循环结束会执行下面逻辑
javascript 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
大于oldEndIdx
说明老节点先遍历完成。然后判断newCh[newEndIdx + 1]
是否有值,如果有值说明剩余的新节点(newStartIdx
到newEndIdx
之间的节点)应该插入到newCh[newEndIdx + 1].elm
前面;反之插入到最后 - 如果
newStartIdx
大于newEndIdx
说明新节点先遍历完成。直接将oldStartIdx
到oldEndIdx
之间的所有节点全部删除
prepatch钩子函数
在patchVnode
方法中,如果新VNode是组件占位符VNode,会调用VNode的prepatch
钩子函数 javascript prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { // 获取组件占位符VNode的 options const options = vnode.componentOptions // 获取组件实例 const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, // updated props 传入子组件的最新的 props 值 options.listeners, // updated listeners 自定义事件 vnode, // new parent vnode options.children // new children ) }
prepatch
钩子函数内调用updateChildComponent
,并传入子组件实例、最新的prop
数据、自定义事件、新VNode和子节点(通过name
属性指定插槽内容的具名插槽)
updateChildComponent
函数如下
```javascript export function updateChildComponent ( vm: Component, // 子组件实例 propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, // 组件 vnode renderChildren: ?Array ) { if (process.env.NODE_ENV !== 'production') { // 设置成 true 的目的是,给 props[key] 赋值时,触发 set 方法,不会让 customSetter 函数报错 isUpdatingChildComponent = true }
const newScopedSlots = parentVnode.data.scopedSlots const oldScopedSlots = vm.$scopedSlots const hasDynamicScopedSlot = !!( (newScopedSlots && !newScopedSlots.$stable) || (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) || (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) )
const needsForceUpdate = !!( renderChildren || // has new static slots vm.$options.renderChildren || // has old static slots hasDynamicScopedSlot ) // vm.$options.parentVnode 指向 新的 组件vnode vm.$options._parentVnode = parentVnode // vm.$vnode 指向 新的 组件vnode vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm.vnode) { // update child tree's parent // 更新 渲染vnode 的 parent vm.vnode.parent = parentVnode } vm.$options._renderChildren = renderChildren
vm.$attrs = parentVnode.data.attrs || emptyObject vm.$listeners = listeners || emptyObject
// update props // 更新 props if (propsData && vm.$options.props) { toggleObserving(false) // 之前的 propsData const props = vm.props // 子组件定义的 props 的属性集合 const propKeys = vm.$options.propKeys || [] for (let i = 0; i < propKeys.length; i++) { const key = propKeys[i] const propOptions: any = vm.$options.props // wtf flow? // 在这里修改props的值触发 组件更新 props[key] = validateProp(key, propOptions, propsData, vm) } toggleObserving(true) vm.$options.propsData = propsData }
// update listeners listeners = listeners || emptyObject // 获取上一次绑定的自定义事件 const oldListeners = vm.$options.parentListeners // 将此次的自定义事件赋值给 _parentListeners vm.$options.parentListeners = listeners updateComponentListeners(vm, listeners, oldListeners)
// resolve slots + force update if has children if (needsForceUpdate) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$forceUpdate() }
if (process.env.NODE_ENV !== 'production') { // 更新完成后,置为 false isUpdatingChildComponent = false } } `` 由于更新了VNode,那么VNode对应实例的一系列属性也会发生变化,包括
vm.$vnode的更新、
slot的更新,
listeners的更新,
props`的更新等等。这些属性的更新会触发子组件更新,具体更新方式在对应文章中都会介绍,这里就不赘述了。
如果子组件需要更新,则将子组件的Render Watcher
直接添加到正在执行的队列中等待执行,而不是调用nextTick
,因为此时queueWatcher
方法的flushing
为true
,会将子组件的Render Watcher
添加到队列的正确位置上;waiting
为true
,不会调用nextTick
方法。
添加到队列后,回到patchVnode
函数,继续更新;当父组件更新完成后,根据队列中的顺序,更新子组件。
新老节点不同
当!isRealElement && sameVnode(oldVnode, vnode)
不成立时,会执行下面的逻辑 ```javascript if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // 根据 oldVnode(此时 oldVnode 是真实节点) 创建一个 vnode oldVnode = emptyNodeAt(oldVnode) } // 获取节点的 真实元素 const oldElm = oldVnode.elm // 获取 oldVnode 的 父节点 const parentElm = nodeOps.parentNode(oldElm)
createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) )
// 更新 组件vnode 的 elm 并重新执行 cbs.create 和 父组件的 insert hooks(不包含 mounted 钩子) if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroyi } // 更新 组件vnode 的 elm ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.createi } const insert = ancestor.data.hook.insert if (insert.merged) { // 从 1 开始,因为第一个insert hook 是 mounted for (let i = 1; i < insert.fns.length; i++) { insert.fnsi } } } else { registerRef(ancestor) } ancestor = ancestor.parent } }
if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } ```
和第一次创建相同,通过createElm
创建节点,如果是组件占位符VNode,则调用init
钩子函数创建组件实例,并执行组件的挂载过程。如果是普通VNode,创建节点,并调用createChildren
创建子节点,将子节点插入到当前节点中。上述执行完成后,将当前节点插入父节点中。
回到patch
函数,更新组件占位符VNode的elm
属性,并重新执行cbs.create
内的函数和组件占位符VNode的insert
钩子函数(不包含mounted
的钩子),最后 删除旧节点,返回最新的DOM树,并赋值给vm.$el
执行insert
钩子的目的是防止出现下面这种情况,如果在组件占位符VNode上有自定义指令,并且insert
回调内绑定了DOM,如果不更新,一直绑定的是老DOM树,所以需要更新 xml <template> <div v-if="xxx">xxx</div> <div v-else>yyy</div> </template>
总结
Vue 的 diff 算法
- 同级比较,然后再去比较子节点
- 判断一方有子节点一方没有子节点的情况
- 比较都有子节点的情况
- 递归比较子节点,具体流程如下
创建4个指针,分别指向新老VNode孩子节点数组的头尾;通过while
循环,遍历数组并垂直、交叉比较
- 如果比较的VNode 相同,则比较该VNode的孩子节点;并修改两个指针的位置。
- 如果垂直、交叉比较没有命中,查看老VNode数组中有没有和新VNode数组的头指针指向的VNode相同的VNode,如果有,比对该VNode的孩子节点。比对完成之后删除老DOM,将新DOM插入到老VNode数组头指针指向的VNode对应DOM的前面;并移动新VNode数组的头指针
- 直至老VNode数组的头指针大于尾指针;或者新VNode数组的头指针大于尾指针
- 老VNode数组的头指针大于尾指针:说明新增了一些DOM。判断新VNode数组尾指针+1是否还有VNode,如果有将这些DOM插入到这个VNode对应的DOM之前;反之插入最后
- 新VNode数组的头指针大于尾指针:说明还有多余的DOM,删除这些DOM
时间复杂度
时间复杂度是O(n)
,只比较同级不考虑跨级问题
v-for
中key
的作用
如果不使用key
,Vue 会尽可能的就地修改/复用相同类型元素的算法。key
是 Vue 中 VNode 的唯一标记,通过这个key
, diff 操作可以更准确、更快速
更准确:因为带key
不是就地复用了,在sameNode
函数a.key === b.key
对比中可以避免就地复用的情况,所以会更加准确
更快速:利用key
的唯一性生成 map 对象来获取对应节点,比遍历方式更快
javascript 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) ) ) ) }
可以看到,key
的优先级高于其他属性