本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 9 篇,关注专栏
前言
上一篇我们分析了 render
函数是如何将 虚拟 DOM
渲染为 真实 DOM
的过程,本篇我们就来看下 render
函数是如何实现 DOM
更新和删除的。
案例
首先引入 h
、 render
两个函数,通过 render
函数先渲染 vnode1
对象,两秒后更新渲染 vnode2
对象。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
<style>
.active {
color: red;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
const { h, render } = Vue
const vnode1 = h(
'div',
{
class: 'test'
},
'hello render'
)
render(vnode1, document.querySelector('#app'))
setTimeout(() => {
const vnode2 = h(
'div',
{
class: 'active'
},
'update'
)
render(vnode2, document.querySelector('#app'))
}, 2000)
</script>
</body>
</html>
render 更新
上篇 Vue3源码解析之 render(一) 中我们了解到,render
函数渲染完毕,最后会挂载旧节点 _vnode
。所以根据案例,首次渲染完后,当前旧节点 _vnode
为 vnode1
对象,两秒后重新执行 render
函数:
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
此时 vnode
参数为 vnode2
对象,由于 vnode
存在,执行 patch
方法:
const patch: PatchFn = (
n1, // 旧节点
n2, // 新节点
container, // 容器
anchor = null, // 锚点
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 新旧节点是否相同
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
// 存在旧节点 且 新旧节点类型是否相同
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
// 根据 新节点类型 判断
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
因为新旧节点 类型相同且值不同
,所以逻辑同上篇相同。当前新节点 type
类型为 div
,执行 default
逻辑中 processElement
方法:
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
// 旧节点不存在 进行 挂载
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
这里需要注意的是,当前存在 旧节点
,所以直接执行 patchElement
方法:
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!) // 将 旧节点的 el 赋值给 n2.el 和 el
let { patchFlag, dynamicChildren, dirs } = n2
// #1426 take the old vnode's patch flag into account since user may clone a
// compiler-generated vnode, which de-opts to FULL_PROPS
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
const oldProps = n1.props || EMPTY_OBJ // 旧节点 props
const newProps = n2.props || EMPTY_OBJ // 新节点 props
let vnodeHook: VNodeHook | undefined | null
// disable recurse in beforeUpdate hooks
parentComponent && toggleRecurse(parentComponent, false)
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
parentComponent && toggleRecurse(parentComponent, true)
if (__DEV__ && isHmrUpdating) {
// HMR updated, force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren) {
// 省略
} else if (!optimized) {
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// 省略
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
const el = (n2.el = n1.el!)
将旧节点的 el
赋值给新节点的 el
:
之后赋值新旧节点的 props
,接着执行 patchChildren
方法来更新子节点:
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children // 旧节点子节点
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children // 新节点子节点
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
// children has 3 possibilities: text, array or no children.
// 新子节点为 text 节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
// 旧子节点为 array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
// 旧子节点为 array 节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
// 新子节点为 array 节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
// 旧子节点为 text 节点
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
// 新子节点为 array 节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
我们主要关注前四个参数,n1
旧节点,n2
新节点,container
为旧节点的 el
,anchor
锚点,之后将新旧子节点赋值给 c1
、 c2
:
由于新子节点为 text
类型,旧子节点不为数组类型,且新旧子节点值不同,所以执行 hostSetElementText(container, c2 as string)
。我们知道 hostSetElementText
方法实际执行的是 setElementText
:
setElementText: (el, text) => {
el.textContent = text
},
当前 el
为旧节点 el
,c2
为新子节点值,重新赋值后页面呈现:
此时新旧子节点更新完毕,之后更新 props
,执行 patchProps
方法:
const patchProps = (
el: RendererElement,
vnode: VNode,
oldProps: Data,
newProps: Data,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean
) => {
// 新旧 props 不同
if (oldProps !== newProps) {
for (const key in newProps) {
// empty string is not valid prop
if (isReservedProp(key)) continue
const next = newProps[key] // 新
const prev = oldProps[key] // 旧
// defer patching value
if (next !== prev && key !== 'value') {
// runtime-dom/src/patchProp.ts 中 patchProp 方法
hostPatchProp(
el,
key,
prev,
next,
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
// 场景 旧节点存在 props 新节点不存在时,则遍历删除
if (oldProps !== EMPTY_OBJ) {
for (const key in oldProps) {
if (!isReservedProp(key) && !(key in newProps)) {
hostPatchProp(
el,
key,
oldProps[key],
null,
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
if ('value' in newProps) {
hostPatchProp(el, 'value', oldProps.value, newProps.value)
}
}
}
当前新旧 props
不同,遍历执行 hostPatchProp
方法来更新,该方法实际执行的是 patchProp
,定义在 packages/runtime-dom/src/patchProp.ts
中。上篇我们也已分析过,这里就不再展开描述。主要是通过 key
类型不同执行不同逻辑,当前 key
为 class
,执行 patchClass
方法:
export function patchClass(el: Element, value: string | null, isSVG: boolean) {
// directly setting className should be faster than setAttribute in theory
// if this is an element during a transition, take the temporary transition
// classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc
if (transitionClasses) {
value = (
value ? [value, ...transitionClasses] : [...transitionClasses]
).join(' ')
}
if (value == null) {
el.removeAttribute('class')
} else if (isSVG) {
el.setAttribute('class', value)
} else {
el.className = value
}
}
此时旧节点的 class
更新为新节点值 active
,页面呈现:
另外还需注意这段判断逻辑 oldProps !== EMPTY_OBJ
,是为了处理新节点不存在 props
属性且旧节点 props
不为空,则遍历删除的情况。
最后 render
函数再将新节点 vnode2
赋值给旧节点 _vnode
,新旧节点更新完毕:
render 更新不同类型
如果我们把案例中 vnode2
类型修改为 h1
,那 render
函数是如何实现更新的呢?
setTimeout(() => {
const vnode2 = h(
'h1',
{
class: 'active'
},
'update'
)
render(vnode2, document.querySelector('#app'))
}, 2000)
render
函数执行 patch
方法中有这么一段代码:
const patch: PatchFn = (
n1, // 旧节点
n2, // 新节点
container, // 容器
anchor = null, // 锚点
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 省略
// patching & not same type, unmount old tree
// 存在旧节点 且 新旧节点类型是否相同
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 省略
}
新旧节点类型不同,会执行 unmount
方法:
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs
} = vnode
// unset ref
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true)
}
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
const shouldInvokeVnodeHook = !isAsyncWrapper(vnode)
let vnodeHook: VNodeHook | undefined | null
if (
shouldInvokeVnodeHook &&
(vnodeHook = props && props.onVnodeBeforeUnmount)
) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component!, parentSuspense, doRemove)
} else {
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.unmount(parentSuspense, doRemove)
return
}
if (shouldInvokeDirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
}
if (shapeFlag & ShapeFlags.TELEPORT) {
;(vnode.type as typeof TeleportImpl).remove(
vnode,
parentComponent,
parentSuspense,
optimized,
internals,
doRemove
)
} else if (
dynamicChildren &&
// #1153: fast path should not be taken for non-stable (v-for) fragments
(type !== Fragment ||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
) {
// fast path for block nodes: only need to unmount dynamic children.
unmountChildren(
dynamicChildren,
parentComponent,
parentSuspense,
false,
true
)
} else if (
(type === Fragment &&
patchFlag &
(PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
(!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
) {
unmountChildren(children as VNode[], parentComponent, parentSuspense)
}
if (doRemove) {
remove(vnode)
}
}
if (
(shouldInvokeVnodeHook &&
(vnodeHook = props && props.onVnodeUnmounted)) ||
shouldInvokeDirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
shouldInvokeDirs &&
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
}, parentSuspense)
}
}
根据类型不同,最终执行 remove
方法:
const remove: RemoveFn = vnode => {
const { type, el, anchor, transition } = vnode
if (type === Fragment) {
if (
__DEV__ &&
vnode.patchFlag > 0 &&
vnode.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT &&
transition &&
!transition.persisted
) {
;(vnode.children as VNode[]).forEach(child => {
if (child.type === Comment) {
hostRemove(child.el!)
} else {
remove(child)
}
})
} else {
removeFragment(el!, anchor!)
}
return
}
if (type === Static) {
removeStaticNode(vnode)
return
}
const performRemove = () => {
hostRemove(el!)
if (transition && !transition.persisted && transition.afterLeave) {
transition.afterLeave()
}
}
if (
vnode.shapeFlag & ShapeFlags.ELEMENT &&
transition &&
!transition.persisted
) {
const { leave, delayLeave } = transition
const performLeave = () => leave(el!, performRemove)
if (delayLeave) {
delayLeave(vnode.el!, performRemove, performLeave)
} else {
performLeave()
}
} else {
performRemove()
}
}
再根据判断逻辑执行 performRemove
方法:
const performRemove = () => {
hostRemove(el!)
if (transition && !transition.persisted && transition.afterLeave) {
transition.afterLeave()
}
}
之后执行 hostRemove
方法,我们知道 host
开头都是浏览器相关操作,被定义在 packages/runtime-dom/src/nodeOps.ts
文件中,实际执行的是 remove
方法:
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
可以看出,该逻辑先获取到当前节点的父节点,之后再将该节点删除:
所以新旧节点类型不同,会直接先删除旧节点,再将旧节点清空 n1 = null
,之后执行 processElement
方法。由于旧节点为空,再执行 mountElement
挂载逻辑,后续逻辑可直接查看上篇,最终页面呈现:
render 删除
我们再修改下案例,将第一个参数传入空值:
setTimeout(() => {
render(null, document.querySelector('#app'))
}, 2000)
render
函数有这么一段逻辑:
const render: RootRenderFunction = (vnode, container, isSVG) => {
// 节点为空
if (vnode == null) {
// 存在旧节点
if (container._vnode) {
// 卸载
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
当传入的 vnode
为空时且存在旧节点,执行 unmount
卸载方法。上面我们已经讲过,该方法会执行 parent.removeChild(child)
删除节点,之后再将 null
赋值给旧节点 _vnode
,render
函数执行完毕。
总结
render
函数更新节点主要通过patchElement
方法来实现。patchElement
方法主要做了patchChildren
更新子节点 和patchProps
更新属性两件事。- 更新新旧节点如果类型不同,
Vue
中会先删除旧节点,再重新挂载新节点。 - 节点的删除实际调用的是
unmount
方法。
Vue3 源码实现
Vue3 源码解析系列
- Vue3源码解析之 源码调试
- Vue3源码解析之 reactive
- Vue3源码解析之 ref
- Vue3源码解析之 computed
- Vue3源码解析之 watch
- Vue3源码解析之 runtime
- Vue3源码解析之 h
- Vue3源码解析之 render(一)
- Vue3源码解析之 render(二)
- Vue3源码解析之 render(三)
- Vue3源码解析之 render(四)
- Vue3源码解析之 render component(一)
- Vue3源码解析之 render component(二)
- Vue3源码解析之 render component(三)
- Vue3源码解析之 render component(四)
- Vue3源码解析之 render component(五)
- Vue3源码解析之 diff(一)
- Vue3源码解析之 diff(二)
- Vue3源码解析之 compiler(一)
- Vue3源码解析之 compiler(二)
- Vue3源码解析之 compiler(三)
- Vue3源码解析之 createApp