patch⼯作原理
patch的核⼼diff算法:通过同层的树节点进⾏⽐较⽽⾮对树进⾏逐层搜索遍历的⽅式,所以 时间复杂度只有O(n),是⼀种相当⾼效的算法。
同层级只做三件事:增删改。具体规则是:new VNode不存在就删;old VNode不存在就增;都存在就 ⽐较类型,类型不同直接替换、类型相同执⾏更新;
下面来看看vue源码中patch方法的实现:
function patch(oldVnode, vnode, hydrating, removeOnly, parentElm,
refElm) {
/*new vnode不存在则删*/
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
/*o ldVnode不存在则创建新节点 */
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
/*oldVnode有nodeType,说明传递进来⼀个DOM元素*/
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
/*是组件且是同⼀个节点的时候打补丁*/
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
/*传递进来oldVnode是真实dom元素*/
if (isRealElement) {
/*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/
oldVnode = emptyNodeAt(oldVnode)
}
/*取代现有元素:*/
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
//创建⼀个新的dom
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
if (isDef(parentElm)) {
/*移除⽼节点*/
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
/*调⽤destroy钩⼦*/
invokeDestroyHook(oldVnode)
}
}
}
/*调⽤insert钩⼦*/
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
在对oldVnode和vnode类型判断中有个sameVnode方法,这个方法决定了是否需要对oldVnode和vnode进行diff及patch的过程。
sameVnode会对传入的两个vnode进行基本属性的比较,只有当基本属性相同的情况下才认为这个两个vnode只是局部发生了更新,然后才会对这两个vnode进行diff,如果两个vnode的基本属性存在不一致的情况,那么就会直接跳过diff的过程,进而依据vnode新建一个真实的dom,同时删除老的dom节点。
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)
)
}
patch vnode实现
两个VNode类型相同,就执⾏更新操作,包括三种类型操作:属性更新PROPS、⽂本更新TEXT、⼦节 点更新REORDER
patchVnode具体规则如下:
- 如果新旧VNode都是静态的,同时它们的key相同(代表同⼀节点),并且新的VNode是clone或 者是标记了v-once,那么只需要替换elm以及componentInstance即可。
- 新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren,这个 updateChildren也是diff的核⼼。
- 如果⽼节点没有⼦节点⽽新节点存在⼦节点,先清空⽼节点DOM的⽂本内容,然后为当前DOM节 点加⼊⼦节点。
- 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该DOM节点的所有⼦节点。
- 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。
/*patch VNode节点*/
function patchVnode(oldVnode, vnode, insertedVnodeQueue,
ownerArray, index, removeOnly) {
/*两个VNode节点相同则直接返回*/
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
/*
如果新旧VNode都是静态的,同时它们的key相同(代表同⼀节点),
并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染⼀次),
那么只需要替换elm以及componentInstance即可。
*/
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
}
/*如果存在data.hook.prepatch则要先执⾏*/
let i
const data = vnode.data
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.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
/*开始判断children的各种情况*/
/*如果这个VNode节点没有text⽂本时*/
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
/*新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren*/
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue,
removeOnly)
} else if (isDef(ch)) {
/*如果⽼节点没有⼦节点⽽新节点存在⼦节点,先清空elm的⽂本内容,然后为当前节点加⼊⼦
节点*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
/*当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除所有ele的⼦节点*/
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
/*当新⽼节点都⽆⼦节点的时候,只是⽂本的替换,因为这个逻辑中新节点text不存在,所以
清除ele⽂本*/
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
/*当新⽼节点text不⼀样时,直接替换这段⽂本*/
nodeOps.setTextContent(elm, vnode.text)
}
/*调⽤postpatch钩⼦*/
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}