Vue 的 keep alive 主要作为缓存 vue 模板组件。
怎么用?
怎么写?
keep-alive 用法,官方文档,直接写标签就可以,因为 keep-alive 和 slot、transition 一样,属于 vue 的内置组件。
<template>
<keep-alive>
<!-- 放置要缓存的组件 -->
</keep-alive>
</template>
怎么配置
可以传三个参数 Props,分别是:
include- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。( 和include用法相同 )max- 数字。最多可以缓存多少组件实例。( 下边会讲LRU算法 )
可以写两个声明周期函数,分别是:
activated- 被keep-alive缓存的组件激活时调用。deactivated- 被keep-alive缓存的组件失活时调用。(使用时,跟data、method平级)
组件内声明周期执行顺序
第一次:created =》mounted =》activated =》deactivated (调用请求数据方法习惯在 mounted 生命周期调用)
第二次:activated =》deactivated
<template>
<!-- include="a,b" :include="/a|b/" :include="['a', 'b']" -->
<keep-alive :max="10" >
<!-- 放置要缓存的组件 只缓存第一个 -->
</keep-alive>
</template>
<script>
export default {
activated() {
console.log('激活调用');
},
deactivated() {
console.log('失活调用');
}
}
</script>
动态组件使用
<!-- currentTabComponent 代表缓存组件的名称-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
用在哪?
结合路由配置,缓存路由匹配的组件
// 路由配置 配置在元信息上 src/route/index.js
routers: [{
path: '/',
name: 'Home',
meta: {
keepAlive: false // 不需要缓存
}
},{
path: '/page',
name: 'Page',
meta: {
keepAlive: true // 需要缓存
}
},]
// 组件配置 src/App.vue
<template>
<div id="home">
<keep-alive>
<!-- 需要缓存 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
</template>
缓存列表页。(数据变动频率不是很快的时候)
- 配置路由
// 配置路由 src/route/index.js
{
path: '/list',
name: 'list',
component: () => import('../views/List.vue'),
meta: {
isCache: false, // 设置在列表组件 默认不缓存
keepAlive: true // 是否使用 keep-alive 缓存
},
children: [
{
path: '/detail',
name: 'detail',
component: () => import('../views/Detail.vue'),
}
]
}
- 编写逻辑
// 列表页面组件 src/views/List.vue
// 激活组件
activated() {
// 判断是否是详情页回退跳转
if(!this.$route.meta.isCache){ //isUseCache 时添加中router中的元信息,判读是否要缓存
this.listData = []; //清空原有数据
this.searchForm = {}; // 清空搜索条件
this.getList(); // 重新加载列表数据
}
// 每次跳转到列表组件时,设置列表组件不缓存,防止非详情页跳转时列表组件仍然渲染缓存的组件
this.$route.meta.isUseCache = false
},
// 列表页面跳转到 详情页时,设置需要缓存
beforeRouteLeave(to, from, next){
// 判断即将跳转的页面是否是详情页
if(to.name=='/list/detail'){
// 设置当前页面缓存
from.meta.isCache = true
}
next()
}
上源码!
源码位置在 src/core/components/keep-alive.js
/* @flow */
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type CacheEntry = {
name: ?string;
tag: ?string;
componentInstance: Component;
};
type CacheEntryMap = { [key: string]: ?CacheEntry };
// 获取当前的组件名称
function getComponentName(opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
// 判断 include 或者 exclude 跟组件的 name 是否匹配成功, props 传参字符串、字符串数组、正则
function matches(pattern: string | RegExp | Array<string>, name: string): boolean {
// ...
}
// prune 修正缓存
function pruneCache(keepAliveInstance: any, filter: Function) {
}
/**
* 清除缓存
*/
function pruneCacheEntry(
) {
// ...
}
const patternTypes: Array<Function> = [String, RegExp, Array]
// 抛出默认组件
export default {
name: 'keep-alive',
abstract: true, // 代表该组件是抽离组件 render 的时候抽离组件不渲染在父组件链上
props: {
include: patternTypes, // 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude: patternTypes, // 字符串或正则表达式。任何名称匹配的组件都不会被缓存。( 和 include 用法相同 )
max: [String, Number] // 数字。最多可以缓存多少组件实例。
},
methods: {
/**
* 清空缓存虚拟DOM
*/
cacheVNode() {
// ...缓存 vnode
}
},
/**
* 创建空对象
*/
created() {
// ... 初始化
},
/**
* keep-alive 销毁生命周期 将所有缓存的组件执行销毁
*/
destroyed() {
// ...销毁
},
/**
* keep-alive
*/
mounted() {
// ...挂载
},
updated() {
// ...更新
},
render() {
// ...渲染
}
}
可以看到默认抛出了一个组件,并且有他对应生命周期的执行逻辑。
初始化
/**
* 初始化
*/
created() {
// 缓存的组件对象
this.cache = Object.create(null)
// 缓存组件的 key
this.keys = []
}
初始化主要生命了两个变量,一个 cache 对象用来缓存被包裹在 keep-alive 组件里的 vnode 实例,一个 keys 数组用来收集所有被缓存实例的 key。
渲染
/**
* 渲染
*/
render() {
// 通过 $slots.default 获得 <keep-alive>内容</keep-alive>
const slot = this.$slots.default
// 获取第一个子组件
const vnode: VNode = getFirstComponentChild(slot)
// 拿到当前组件 options
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
// 获取传入的 props
const { include, exclude } = this
// 不缓存的情况
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
// 根据组件 options ,获取组件缓存 key
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
// 相同的构造函数可以注册为不同的本地组件 所以单独一个 cid 不能满足 需要 cid + 当前组件 tag
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 看当前缓存里有没有缓存实例 有的话更新位置 (LRU 算法)
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// * make current key freshest 直译:使当前key称为最新鲜的
// 删除当前缓存
remove(keys, key)
// 添加到队列最后一位
keys.push(key)
} else {
// delay setting the cache until update
// 没有当前实例就缓存当前实例
this.vnodeToCache = vnode
// 把当前实例的 key
this.keyToCache = key
}
// keepAlive 标记位
vnode.data.keepAlive = true
}
// 返回子组件或者返回插槽的第一个元素
return vnode || (slot && slot[0])
}
- 获取插槽中第一个的组件
- 当前组件没有在缓存的配置项中,直接返回
- 看看之前有没有被缓存过,如果被缓存刷新当前组件在缓存队列里的位置(LRU算法),没有被缓存过就设置当前组件为接下来要被缓存
- 返回缓存的 vnode
挂载
/**
* 挂载
*/
mounted() {
// 缓存 vnode
this.cacheVNode()
// 使用 vue 的监听函数,监听 include 的最新值 修正需要缓存的组件
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
// 使用 vue 的监听函数,监听 exclude 的最新值 修正不需要缓存的组件
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
挂载 keep-alive 到页面主要做了三件事
缓存 vnode
/**
* 缓存 vnode
*/
cacheVNode() {
// 从当前实例中解构出 缓存对象 缓存keys 将要被缓存的 vode 和将要被缓存的 key
const { cache, keys, vnodeToCache, keyToCache } = this;
// 如果之前在 render 函数中有将要缓存的组件
if (vnodeToCache) {
// 缓存组件实例
const { tag, componentInstance, componentOptions } = vnodeToCache
// 把当前缓存的对象挂到 cache 对象上,key 是啥看 render 函数注解
cache[keyToCache] = {
name: getComponentName(componentOptions), // 获取组件的名称
tag, // 组件标签
componentInstance, // 组件实例
}
// 把当前 key 缓存到 keys 数组里
keys.push(keyToCache)
// prune oldest entry
// 判断当前最大缓存数 如果当前缓存的数量大于设置的最大缓存数 缓存之前没有命中的缓存
if (this.max && keys.length > parseInt(this.max)) {
// 清除当前第一个缓存组件 (见下方 LRU 算法图)
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
// 清除将要缓存 vnode 的变量
this.vnodeToCache = null
}
}
}
这个方法每次走到 mounted 生命周期的时候都会执行,mounted 生命周期没走到之前,先走到的时候 render 函数,render 函数里以及把将要缓存的组件给了当前组件变量 vnodeToCache ,依据 vnodeToCache 我们就可以知道当前是否有要被缓存的组件,有的话挂到 cache 对象上,并且把当前组件的 key 添加到 keys 的最后一位,查看当前缓存的数量是否大于 max 最大缓存量,大于的话清除掉第一个(LRU算法,别急,最后说)。最后清除将要被缓存的 vnode 变量,方便下一次缓存。
监听传入的 Props
/**
* 挂载
*/
mounted() {
// 缓存 vnode
this.cacheVNode()
// 使用 vue 的监听函数,监听 include 的最新值 修正需要缓存的组件
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
// 使用 vue 的监听函数,监听 exclude 的最新值 修正不需要缓存的组件
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
为啥要监听呢,因为可能每次上次和这一次缓存的组件不同,所以要及时修正缓存组件的容器 cache 和 keys。
这里用到了两个方法一个是 pruneCache 修正缓存(prune 本身的意思是修剪),另一个是 matches 匹配。
// prune 修正缓存
function pruneCache(keepAliveInstance: any, filter: Function) {
// 当前 keep-alive 实例 由 vnode 组成的缓存对象和 keys 和 _vnode
const { cache, keys, _vnode } = keepAliveInstance
// 循环遍历
for (const key in cache) {
// 获取每个 vnode
const entry: ?CacheEntry = cache[key]
// 如果 vnode 存在
if (entry) {
const name: ?string = entry.name
// 当前 vnode 存在,查看是否和传入的 include 和 exclude 匹配
if (name && !filter(name)) {
// 不匹配销毁实例
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
简单说,就是看一下组件里是否有不需要缓存的组件,有的话就销毁。
// 判断 include 或者 exclude 跟组件的 name 是否匹配成功, props 传参字符串、字符串数组、正则
function matches(pattern: string | RegExp | Array<string>, name: string): boolean {
// 判断 Props 是否为数组
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
// 判断 Props 是否为字符串
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
// 判断 Props 是否为正则
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
// 以上三种条件都没匹配上 返回 false
return false
}
判断一下当前 vnode 的 name 是否是要被缓存的,从三个规则去校验,字符串、正则、字符串数组,这三个规则也是 include 和 exclude 传递 Props 三种不同的类型。
更新
/**
* 更新
*/
updated() {
// 更新生命周期 缓存 vnode
this.cacheVNode()
}
更新的时候也看一下是否有组件要被缓存,清除一下长时间未激活的缓存组件。
销毁
/**
* keep-alive 销毁生命周期 将所有缓存的组件执行销毁
*/
destroyed() {
// 循环遍历删除所有缓存
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
keep-alive 需要被销毁的时候,循环遍历所有缓存的组件和 key
/**
* 清除缓存
*/
function pruneCacheEntry(
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
// 获取当前实例
const entry: ?CacheEntry = cache[key]
// 有当前实例 且 当前 VNode 不存在 或 当前标签 不等于 当前
if (entry && (!current || entry.tag !== current.tag)) {
// 执行当前组件的 destroy 销毁生命周期
entry.componentInstance.$destroy()
}
// 设置当前缓存的实例等于 null ,清除缓存
cache[key] = null
// 删除 remove 方法引用了公共方法里的 remove ,通过数组的 splice 方法实现
remove(keys, key)
}
LRU 算法
算法的精髓的在于思路,理解思路最好的方法在于看图。
凑合看哈,就是这么一个意思。LRU 算法也称为缓存淘汰算法,清除最少被使用的组件,核心思想是“如果组件最近被命中激活,那么将来被命中激活的几率也更高”。
keep-alive 中,使用队列的形式缓存组件,每次都把最不经常命中激活的第一个组件“推”出队列,最经常被命中激活的组件会被添加到队列最后一位。