keep-alive是Vue.js的一个内置组件。它能够不活动的组件实例保存在内存中,我们来探究一下它的源码实现。
首先回顾下使用方法
举个栗子
<keep-alive>
<component-a v-if="isShow"></component-a>
<component-b v-else></component-b>
</keep-alive>
<button @click="test=handleClick">请点击</button>
export default {
data () {
return {
isShow: true
}
},
methods: {
handleClick () {
this.isShow = !this.isShow;
}
}
}
在点击按钮时,两个组件会发生切换,但是这时候这两个组件的状态会被缓存起来,比如:组件中都有一个input标签,那么input标签中的内容不会因为组件的切换而消失。
属性支持
keep-alive组件提供了include
与exclude
两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。
举个例子:
- 缓存
name
为a的组件。
<keep-alive include="a">
<component></component>
</keep-alive>
- 排除缓存name为a的组件。
<keep-alive exclude="a">
<component></component>
</keep-alive>
当然 props 还定义了 max,该配置允许我们指定缓存大小。
keep-alive 源码实现
说完了keep-alive组件的使用,我们从源码角度看一下keep-alive组件究竟是如何实现组件的缓存的呢?
创建和销毁阶段
首先看看 keep-alive
的创建和销毁阶段做了什么事情:
created () {
/* 缓存对象 */
this.cache = Object.create(null)
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache[key])
}
},
- 在
keep-alive
的创建阶段: created钩子会创建一个cache
对象,用来保存vnode节点。 - 在销毁阶段:destroyed 钩子则会调用
pruneCacheEntry
方法清除cache缓存中的所有组件实例。
pruneCacheEntry
方法的源码实现
/* 销毁vnode对应的组件实例(Vue实例) */
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) {
vnode.componentInstance.$destroy()
}
}
因为keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法,因此keep-alive提供了两个生命钩子,分别是activated
与deactivated
。用这两个生命钩子得知当前组件是否处于活动状态。(稍后会看源码如何实现)
渲染阶段
render () {
/* 得到slot插槽中的第一个组件 */
const vnode: VNode = getFirstComponentChild(this.$slots.default)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 获取组件名称,优先获取组件的name字段,否则是组件的tag
const name: ?string = getComponentName(componentOptions)
// 不需要缓存,则返回 vnode
if (name && (
(this.include && !matches(this.include, name)) ||
(this.exclude && matches(this.exclude, name))
)) {
return vnode
}
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (this.cache[key]) {
// 有缓存则取缓存的组件实例
vnode.componentInstance = this.cache[key].componentInstance
} else {
// 无缓存则创建缓存
this.cache[key] = vnode
// 创建缓存时
// 如果配置了 max 并且缓存的长度超过了 this.max
// 则从缓存中删除第一个
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(this.cache, keys[0], keys, this._vnode)
}
}
// keepAlive标记
vnode.data.keepAlive = true
}
return vnode
}
render 做了以下事情:
- 通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)
- 将name通过include与exclude属性进行匹配,匹配不成功(说明不需要缓存)则直接返回vnode
- 匹配成功则尝试获取缓存的组件实例
- 若没有缓存该组件,则缓存该组件
- 缓存超过最大值会删掉第一个缓存
name 匹配的方法(校验是逗号分隔的字符串还是正则)
/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
if (typeof pattern === 'string') {
/* 字符串情况,如a,b,c */
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
/* 正则 */
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
如果在中途有对 include
和 exclude
进行修改该怎么办呢?
作者通过 watch
来监听 include
和 exclude
,在其改变时调用 pruneCache
以修改 cache
缓存中的缓存数据。
watch: {
/* 监视include以及exclude,在被修改的时候对cache进行修正 */
include (val: string | RegExp) {
pruneCache(this.cache, this._vnode, name => matches(val, name))
},
exclude (val: string | RegExp) {
pruneCache(this.cache, this._vnode, name => !matches(val, name))
}
},
那么 pruneCache
做了什么?
// 修补 cache
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
for (const key in cache) {
// 尝试获取 cache中的vnode
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) { // 重新筛选组件
if (cachedNode !== current) { // 不在当前 _vnode 中
pruneCacheEntry(cachedNode) // 调用组件实例的 销毁方法
}
cache[key] = null // 移除该缓存
}
}
}
}
pruneCache
方法 遍历cache中的所有项,如果不符合规则则会销毁该节点并移除该缓存
进阶
再回顾下源码,在 src/core/components/keep-alive.js
中
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 () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
现在不加注释也应该大部分都能看懂了?
顺便提下 abstract
这个属性,若 abstract
为 true
,则表示组件是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。
那么为什么在组件有缓存的时候不会再次执行组件的 created
、mounted
等钩子函数呢?
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// 进入这段逻辑
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// ...
}
看上面了代码, 满足 vnode.componentInstance
&& !vnode.componentInstance._isDestroyed
&& vnode.data.keepAlive
的逻辑就不会执行$mount
的操作,而是执行prepatch
。
那么 prepatch
究竟做了什么?
// 不重要内容都省略...
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// 会执行这个方法
updateChildComponent(//...)
},
// ...
其中主要是执行了 updateChildComponent
函数。
function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
const hasChildren = !!(
renderChildren ||
vm.$options._renderChildren ||
parentVnode.data.scopedSlots ||
vm.$scopedSlots !== emptyObject
)
// ...
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
}
keep-alive
组件本质上是通过 slot
实现的,所以它执行 prepatch
的时候,hasChildren = true
,会触发组件的 $forceUpdate
逻辑,也就是重新执行 keep-alive
的 render 方法
然鹅,根据上面讲的 render 方法源码,就会去找缓存咯。
那么,<keep-alive>
的实现原理就介绍完了
最后
- 原创不易点个赞呗
- 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。