文章内容
用具体的例子讲解patchVNode的具体流程,通过例子逐渐分析出patchVNode代码所代表的含义
前置知识
由上一篇文章Vue2源码-整体流程浅析可以知道,当两个VNode是同一个VNode时,会触发patchVNode()的执行
prepatch()触发
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
//...
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
//...
}
从下面代码可以知道,prepatch()
主要是调用了updateChildComponent()
,这个方法的作用是将newVnode
相关数据更新到旧的oldVnode.componentOptions
// vue/src/core/vdom/create-component.js
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
// vue/src/core/instance/lifecycle.js
export function updateChildComponent(
vm: Component, // 旧的Component对象
propsData: ?Object, // 新的props数据
listeners: ?Object, // 新的listeners数据
parentVnode: MountedComponentVNode, // 新的VNode数据
renderChildren: ?Array<VNode> // 新的children数据
) {
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._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[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
vm.$options.propsData = propsData
}
//...省略代码,基本跟上面代码逻辑一致,进行vm的更新而已
}
触发update()方法执行
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
//...
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
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)
}
//...
}
参考Vue2自定义指令的钩子函数:https://v2.cn.vuejs.org/v2/guide/custom-directive.html#ad
从文档可以知道,update()
的钩子函数,代表所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
比如下面的示例代码,如果我们注册了一个test33
的自定义指令,那么<div v-test33></div>
所在组件的 VNode 更新时调用注册的update()
方法
// html
<template>
<div v-test33></div>
</template>
// js
Vue.directive('test33', {
update: function (el) {
console.error("测试update 自定义指令");
}
})
分情况进行children的比较
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
//...
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
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)
}
}
从上面的代码可以知道,当prepatch()
将newVNode
的相关数据更新到oldVNode.componentOptions
后,会进行它们children
的比较,确定它们children
的更新策略,总结下来为
- oldVNodeChildren!==newVNodeChildren,触发
updateChildren()
方法 - oldVNodeChildren为空,newVNodeChildren不为空,执行newVNodeChildren新建插入操作
- oldVNodeChildren不为空,newVNodeChildren为空,执行oldVNodeChildren删除操作
- 如果是文本节点,则更新文本内容
核心部分updateChildren()
前置说明
- 由于流程过于复杂,将使用一个具体的例子说明源码的流程
patchVnode(oldVnode, newVnode, insertedVnodeQueue, newCh, newIdx):当
oldVnode
=newVnode
的时候,才会触发patchVnode()
方法,进行新旧VNodeData
的更新,然后进行children
元素的比较,进行新增删除/向下比较触发updateChildren()
,除了更新数据(同个VNode进行数据更新)、处理children(addVNode/removeVNode/updateVNode-选择性更新),不做其它处理下面所有图中旧的children真实情况下应该存在oldVNode的DOM引用情况,为了偷懒,会将矩形外边的节点当作oldVNode引用DOM的位置,矩形包裹才是旧VNode的位置,因此会出现情况5中就算移动了元素到其它位置,仍然存在元素在oldVNode中的情况
条件分类代码
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
} else {
// 情况5: 四端都找不到同样的VNode
}
}
情况4: oldEndIdx===newStartIdx
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
// 数据更新,如果有children,也在patchVnode()内部进行调用处理,addVNode/removeVNode/updateChildren
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 调整目前DOM元素的位置,将oldEndVnode的DOM移动到oldStartVnode的DOM前面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx] // 更新索引
newStartVnode = newCh[++newStartIdx] // 更新索引
} else {
// 情况5: 四端都找不到同样的VNode
}
}
情况2: oldEndIdx===newEndIdx
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
// 数据更新,如果有children,也在patchVnode()内部进行调用处理,addVNode/removeVNode/updateChildren
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
//......
} else {
// 情况5: 四端都找不到同样的VNode
}
}
情况3: oldStartIdx === newEndIdx
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
//....
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
// 数据更新,如果有children,也在patchVnode()内部进行调用处理,addVNode/removeVNode/updateChildren
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 调整目前DOM元素的位置,将oldStartVnode的DOM移动到oldEndVnode的DOM后面
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
//......
} else {
// 情况5: 四端都找不到同样的VNode
}
}
情况1: oldStartIdx === newStartIdx
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
// 数据更新,如果有children,也在patchVnode()内部进行调用处理,addVNode/removeVNode/updateChildren
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
//....
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
//....
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
//......
} else {
// 情况5: 四端都找不到同样的VNode
}
}
情况5: 四端都无法找到可以复用,相同的VNode
四端无法找到可匹配的VNode,但是中间可以找到
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
} else {
// 情况5: 四端都找不到同样的VNode
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
// ...
} else {
// 命中下面代码
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 数据更新,如果有children,也在patchVnode()内部进行调用处理,addVNode/removeVNode/updateChildren
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 将索引中的oldCh[idxInOld]置为空,方便后面直接跳过
oldCh[idxInOld] = undefined
// 调整目前DOM元素的位置,将oldCh[idxInOld]的DOM移动到oldStartVnode前面
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]
}
}
(新条件1)oldStartVnode=undefine:中间区域可复用区域直接跳过下一次循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 新条件1:oldStartVnode=undefine
oldStartVnode = oldCh[++oldStartIdx] // 情况5置为undefined的startVNode
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
} else {
// 情况5: 四端都找不到同样的VNode
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
// ...
} else {
// 命中下面代码
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// ...
oldCh[idxInOld] = undefined
// ...
} else {
// ...
}
}
newStartVnode = newCh[++newStartIdx]
}
}
四端无法找到可匹配的VNode,中间也无法找到,属于新增元素
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 情况5置为undefined的startVNode
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
} else {
// 情况5: 四端都找不到同样的VNode
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 命中下面代码,新增元素并且插入oldStartVNode对应的DOM前面
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// ...
oldCh[idxInOld] = undefined
// ...
} else {
// ...
}
}
newStartVnode = newCh[++newStartIdx]
}
}
(循环结束1)newStartIdx > newEndIdx
结束第一个循环需要删除不用的旧VNode
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 新条件1:oldStartVnode=undefine
oldStartVnode = oldCh[++oldStartIdx] // 情况5置为undefined的startVNode
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
} else {
// 情况5: 四端都找不到同样的VNode,中间可复用,移动DOM/中间不可复用,新增插入DOM
if (isUndef(idxInOld)) {
// 四端都找不到同样的VNode,中间不可复用,新增插入DOM
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 四端都找不到同样的VNode,中间可复用,移动DOM
} else {
// key相同,其它条件不同当作新VNode处理,即中间不可复用,新增插入DOM处理
}
}
}
}
if (newStartIdx > newEndIdx) {
// 循环结束1:删除已经废弃的旧VNode
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
从上面分析代码可以看出,条件中还缺少oldEndVnode
不存在以及结束第一个循环时,oldStartIdx > oldEndIdx
两种可能性的判断,事实上这两种情况也有可能发生
(新条件2)oldEndVnode=undefine
暂时还没想好场景,先作为补齐条件判断加入
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 新条件1:oldStartVnode=undefine
oldStartVnode = oldCh[++oldStartIdx] // 情况5置为undefined的startVNode
} else if (isUndef(oldEndVnode)) {
// 新条件2:oldEndVnode=undefine
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况1: oldStartIdx === newStartIdx
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况2: oldEndIdx === newEndIdx
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况3: oldStartIdx === newEndIdx
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况4: oldEndIdx === newStartIdx
} else {
// 情况5: 四端都找不到同样的VNode,中间可复用,移动DOM/中间不可复用,新增插入DOM
if (isUndef(idxInOld)) {
// 四端都找不到同样的VNode,中间不可复用,新增插入DOM
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 四端都找不到同样的VNode,中间可复用,移动DOM
} else {
// key相同,其它条件不同当作新VNode处理,即中间不可复用,新增插入DOM处理
}
}
}
}
if (newStartIdx > newEndIdx) {
// 删除已经废弃的旧VNode
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
(循环结束2)oldStartIdx>oldEndIdx
由上面的分析可以知道,一开始会触发情况1: oldStartIdx === newStartIdx,然后oldStartIdx++
和oldEndIdx++
,此时由于oldStartIdx>oldEndIdx
,循环结束