高频面试题:
keep-alive是如何实现的?
vue中支持组件化,并且也有用于缓存的内置组件keep-alive可直接使用,使用场景为路由组件和动态组件,接下来举个动态组件的例子
// main.js文件
import Vue from "vue";
let A = {
template: '<p>component-a</p>',
name: 'A',
}
let B = {
template: '<p>component-b</p>',
name: 'B',
}
new Vue({
el: '#app',
template: '<div><keep-alive><component :is="currentComponent"></component></keep-alive><button @click="change">switch</button></div>',
data: {
currentComponent: 'A'
},
methods: {
change() {
this.currentComponent = this.currentComponent === 'A' ? 'B' : 'A'
}
},
components: {
A,
B
}
})
一、注册
在initGlobalAPI(Vue)阶段,通过extend(Vue.options.components, builtInComponents)的方式在Vue.options中扩展components,builtInComponents指的就是KeepAlive:
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
// 会在后面介绍
}
}
keep-alive组件是一个包含name、abstract、props、created、destroyed、mounted和render属性的对象。
二、首次渲染
1、div渲染逻辑中的render
在执行vm._update(vm._render(), hydrating)进行渲染的过程中,编译生成的render函数为:
with(this) {
return _c('div', [
_c('keep-alive', [_c(currentComp, {
tag: "component"
})], 1),
_c('button', {
on: {
"click": change
}
}, [_v("switch")])
], 1)
}
可以看出通过_c创建了tag为div的节点,节点中children部分为通过_c创建的以keep-alive为tag的节点,其节点中children部分为通过_c创建的以currentComp(首次渲染时其值为A)为标签的节点。
换个角度说,div为根节点,keep-alive为第二层,currentComp为叶子层。
当前渲染中遇到div标签时,会执行createElm逻辑中的createChildren部分逻辑:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
这里会有两个vNode节点需要渲染,通过for循环的方式依次去调用createElm方法,我们重点关注keep-alive。
2、keep-alive渲染逻辑中的render
当遇到keep-alive对应的标签时,会执行createComponent逻辑:
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
}
}
}
其中的i指的是钩子函数init。
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)
}
},
init阶段通过createComponentInstanceForVnode先创建child的实例,在当前过程中会执行组件构造函数的this._init方法,其中initRender时会通过vm.$slots = resolveSlots(options._renderChildren, renderContext)的方式解析slot:
export function resolveSlots (
children: ?Array<VNode>,
context: ?Component
): { [key: string]: Array<VNode> } {
if (!children || !children.length) {
return {}
}
const slots = {}
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
const data = child.data
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
const name = data.slot
const slot = (slots[name] || (slots[name] = []))
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
(slots.default || (slots.default = [])).push(child)
}
}
// ignore slots that contains only whitespace
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
这里通过(slots.default || (slots.default = [])).push(child)的方式将vnode推入到slots的default中。
然后,通过child.$mount(hydrating ? vnode.elm : undefined, hydrating)的方式去挂载子组件节点到vnode.elm上。其中,会走到渲染逻辑,获取到的render为keep-alive内置组件中的render函数:
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
首先,通过const slot = this.$slots.default获取slot。
然后,通过getFirstComponentChild获取到第一个vNode,逻辑如下:
export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
接着就是exclude和include属性,如果不在include中或者在exclude中,直接返回当前vnode。
再看缓存组件,在缓存内直接返回vnode,并修改缓存vnode的顺序;如果不在缓存内则进行缓存处理。
最后修改vnode.data中的keepAlive为true。
3、currentCom渲染中的render
当渲染currentCom(首次渲染时为A)时,会执行createComponent逻辑。在当前阶段,触发init的钩子函数,通过createComponentInstanceForVnode先创建child的实例。
然后,通过child.$mount(hydrating ? vnode.elm : undefined, hydrating)的方式去挂载子组件节点到vnode.elm上。挂载过程中获取到的render为:
with(this) {
return _c('p', [_v("component-a")])
}
至此可以看出,从div到keep-alive再到currentCom的过程中,是从根节点开始到叶子节点路线开始,再通过递归的方式由叶子节点获取DOM节点开始一步一步的获取到根节点的方式,完成整棵dom树的构建。
三、缓存组件
A组件和B组件都完成渲染后,再次执行到keep-alive组件渲染时,vnode获取过程中render函数通过vnode.componentInstance = cache[key].componentInstance在vnode中挂载缓存中的componentInstance(在首次渲染时已经完成了$el的渲染)。
在keep-alive组件执行钩子函数init阶段,满足条件if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ),执行var mountedNode = vnode; componentVNodeHooks.prepatch(mountedNode, mountedNode),即执行其钩子函数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
)
},
updateChildComponent的主要逻辑为:
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// ...
// 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()
}
}
满足需要强制更新的条件,通过vm.$slots = resolveSlots(renderChildren, parentVnode.context)为当前vm中定义$slots,并通过vm.$forceUpdate()强制更新vm,在下一个队列queue中推入一个watcher。
执行到组件currentCom(当前为A)的渲染时,也执行createComponent的逻辑:
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
}
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
执行i(init钩子函数)的时候执行完init函数以后,满足条件isDef(vnode.componentInstance),接着执行initComponent(vnode, insertedVnodeQueue),这里将缓存组件实例中渲染的$el直接赋值给vnode.elm。
然后再通过insert(parentElm, vnode.elm, refElm)的方式将缓存vnode中的elm挂载到父节点中,我们发现,该过程没有进行A组件的实例化和挂载的过程,而是直接进行缓存节点载入,是平时项目开发中的一种性能优化的方案。
总结
keep-alive作为内置组件,通过render函数实现keep-alive中第一个组件的缓存、include匹配到组件的进行缓存、exclude排除的组件不进行缓存和max限制最多能缓存数量的功能。