这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战
前言
上篇已经看过Vue内置的抽象组件keep-alive内部的具体实现,但是我们可能更关心的是被keep-alive包裹的组件是怎么做处理的。
这篇来看下被keep-alive缓存的组件是具体怎么渲染的,与平时我们写的普通组件有什么不同??
组件创建
在前边看Vue diff的时候已经知道Vue的_update方法是用来把虚拟DOM渲染为真实DOM的,它内部就是执行patch的具体过程。
所有的创建节点最后都会走到createElm函数,包括组件。createElm定义在src/core/vdom/patch.js中。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
看到创建组件主要是调用了createComponent方法,也定义在src/core/vdom.patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
从代码看到,createComponent函数中定义了一个与缓存组件有关的变量:isReactivated
-
初次渲染的时候,vnode.componentInstance是undefined
-
由于<keep-alive>组件会先比被它包裹的组件先执行,所以在执行keep-alive组件的render函数时,会把该组件Vnode缓存到自己定义的cache对象中,并将vnode.data.keepAlive设置为true
-
所以初次渲染的时候isReactivated为false,会跟普通组件一样走正常的init过程。
组件切换
在分析diff的过程中我们知道对于相同类型的节点会执行patchVnode方法,该方法会去对比新老节点以及它们的子节点(updateChildren)。但是组件Vnode是没有children的(children参数为undefined),那被keep-alive包裹的组件在切换时,怎么更新呢??
当初在看这个方法的时候只看了后边对新老节点的对比,没有关注代码前边部分。其实代码前边有如下一段:
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
...
}
在执行节点对比之前,会先执行prepatch钩子函数,定义在src/core/vdom/create-component.js
prepatch
prepatch函数定义在componentVNodeHooks中:
const componentVNodeHooks = {
...
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
)
}
...
}
prepatch钩子函数内部主要是执行了updateChildComponent方法,定义在src/core/instance/lifecycle.js
updateChildComponent
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren.
// check if there are dynamic scopedSlots (hand-written or compiled but with
// dynamic slot names). Static scoped slots compiled from template has the
// "$stable" marker.
const newScopedSlots = parentVnode.data.scopedSlots
const oldScopedSlots = vm.$scopedSlots
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
)
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
...
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
}
keep-alive组件是支持slot的,所以在执行prepatch方法的时候会去执行$forceUpdate逻辑。forceUpdate后边单独看,它的主要作用就是强制触发依赖更新。
这个时候又会去执行<keep-alive>的render函数,如果包裹的组件已经被缓存过,则直接执行vnode.componentInstance = cache[key].componentInstance
此时又回到最开始的createComponent方法,首先执行init方法,定义在src/core/vdom/create-component.js中
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
作为缓存组件会直接执行prepatch方法,看一下这个prepatch做了什么:
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
)
}
prepatch方法内部是调用了updateChildComponent方法,即调用了$forceUpdate去更新组件。从这里可以看出keep-alive组件在命中缓存以后,在执行createComponent函数时是不会像普通组件一样去执createComponentInstanceForVnode,这也就是为什么缓存组件内部切换时,不会再去调用created,mounted等钩子了。
继续看,这时候的isReactivated为true,会执行下边的代码:
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
看一下这个reactivateComponent函数做了什么:
reactivateComponent
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i
// hack for #4339: a reactivated component with inner transition
// does not trigger because the inner node's created hooks are not called
// again. It's not ideal to involve module-specific logic in here but
// there doesn't seem to be a better way to do it.
let innerNode = vnode
while (innerNode.componentInstance) {
innerNode = innerNode.componentInstance._vnode
if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
for (i = 0; i < cbs.activate.length; ++i) {
cbs.activate[i](emptyNode, innerNode)
}
insertedVnodeQueue.push(innerNode)
break
}
}
// unlike a newly created component,
// a reactivated keep-alive component doesn't insert itself
insert(parentElm, vnode.elm, refElm)
}
从注释可以看到reactivateComponent函数主要做了两件事:
-
解决reactivated组件中transition不触发的问题(原因就是created钩子不再调用导致的)。
-
执行insert方法将缓存组件的DOM元素插入到对应的位置。
总结
-
缓存组件在初次执行时除了会被keep-alive缓存外,与普通组件是一样的。
-
组件Vnode不存在children,在执行diff之前会先调用prepatch方法,更新组件时会直接调用updateChildComponent
-
缓存组件在命中缓存后不会再去执行created,mounted等方法,而是直接调用$forceUpdate去更新组件。