文章内容
用具体的例子解说 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
,循环完结
从上图能够看出,目前 newCh
还有一个 VNode
未解决,因而须要遍历 [newStartIdx, newEndIdx]
进行残余元素的增加解决,如上面代码所示,循环区间,进行 createElm()
操作
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 (oldStartIdx > oldEndIdx) {
// 如果新的 children 最初一个节点曾经解决实现,那么初始化 refElm 为最初一个节点
// 由上面 insert 办法能够晓得,插入元素会 nodeOps.insertBefore(parent, elm, ref)
// 一直在 ref 后面插入 [newStartIdx,newEndIdx] 的 DOM
// 因为 ref 统一都不会动,因而 [newStartIdx,newEndIdx] 能够程序插入
// 如果新的 children 最初一个节点还没解决,那么解决 refElm=null
// 由上面 insert 办法能够晓得,插入元素会 nodeOps.appendChild(parent, elm)
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// addVnodes:[newStartIdx,newEndIdx]遍历调用 createElm 插入 DOM 元素
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {// 循环完结 1: 删除多余的 oldVNode}
// addVnodes->createElm()->insert()
function insert(parent, elm, ref) {if (isDef(parent)) {if (isDef(ref)) {if (nodeOps.parentNode(ref) === parent) {nodeOps.insertBefore(parent, elm, ref)
}
} else {nodeOps.appendChild(parent, elm)
}
}
}
触发 postpatch()执行
if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
参考 Vue2 自定义指令的钩子函数:https://v2.cn.vuejs.org/v2/guide/custom-directive.html#ad
从上面的源码剖析和文档能够晓得,postpatch()
实际上调用的是 componentUpdated()
的钩子函数,代表指令所在组件的 VNode 及其子 VNode 全副更新后调用
if (dirsWithPostpatch.length) {mergeVNodeHook(vnode, 'postpatch', function () {for (var i = 0; i < dirsWithPostpatch.length; i++) {callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
比方上面的示例代码,如果咱们注册了一个 test33
的自定义指令,那么 <div v-test33></div>
所在组件的 VNode 及其子 VNode 全副更新后就会调用注册的 componentUpdated()
办法
// html
<template>
<div v-test33></div>
</template>
//js
Vue.directive('test33', {componentUpdated: function () {console.error("测试 componentUpdated");
}
})
参考文章
- vue-design