概念
在使用vue时,组件之间切换,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
我们可以用一个 <keep-alive>
元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!-->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
它接收三个参数:
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max - 数字。最多可以缓存多少组件实例。
<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
当组件在 <keep-alive>
内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
源码实现
export default {
name: 'keep-alive',
abstract: true,
props: {
include: [String, RegExp, Array], // 包含 可传字符串、正则表达式、数组
exclude: [String, RegExp, Array], // 排除
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 = getFirstComponentChild(slot) // 获取插槽的第一个组件选项
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name = getComponentName(componentOptions)
const { include, exclude } = this
if (
// 没有包括在内
(include && (!name || !matches(include, name))) ||
// excluded 排除的
(exclude && name && matches(exclude, name))
) {
// 直接返回节点
return vnode
}
const { cache, keys } = this
// 同一个构造函数可能被注册为不同的局部组件
// 所以只有cid是不够的 (#3269)
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// LRU算法
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])
}
}
代码很简单,就是初始化一个对象用来缓存组件节点,根据传进来的参数判断哪些组件是否要缓存,如果不需要缓存,则直接返回节点, 否则读取缓存中的节点。
在缓存超过max
时,使用了缓存淘汰算法LRU,其实就是删了再加上,超过就删掉缓存头部节点。
其中的工具函数
// 获取组件名字
function getComponentName (opts) {
return opts && (opts.Ctor.options.name || opts.tag)
}
// 匹配
function matches (pattern, name){
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
// 修剪缓存
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
// 修剪缓存入口
function pruneCacheEntry ( cache, key, keys, current) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
// 是否是正则类型
export function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
/**从数组中删除项
* Remove an item from an array
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
// 获取第一个 组件
export function getFirstComponentChild (children) {
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
}
}
}
}
export function isAsyncPlaceholder (node) {
return node.isComment && node.asyncFactory
}
export function isDef (v) {
return v !== undefined && v !== null
}
那么keep-alive
是如何触发这两个activated
和 deactivated
钩子的呢?
在组件最后我们可以看到
vnode.data.keepAlive = true
在源码中通过搜索keepAlive
可以定位到这些代码:
keepAlive为true,它会调一个方法
依着这个方法继续找
好像快找到了, 这里调用所有active钩子
最后找到了
也就是说,通过往实例上设置个属性keepAlive
来判断是否触发activated
钩子。