啥玩意
kee-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存
用法及参数
<keep-alive>
<component />
</keep-alive>
<!-- vue3 -->
<router-view v-slot="{ Component }">
<keep-alive>
<component
:is="Component"
/>
</keep-alive>
</router-view>
参数:
- include包含的组件(类型:字符串,数组,正则表达式;只有匹配的组件会被缓存)
- exclude排除的组件(类型:字符串,数组,正则表达式;任何匹配的组件都不会被缓存,优先级高于include)
- max缓存组件的最大值(类型:字符,数字;可以控制缓存组件的个数,超过会删除最开始缓存的)
include和exclude参数匹配的是组件name字段,并不是路由name
钩子函数
- activated 当 keepalive 包含的组件再次渲染的时候触发
- deactivated 当 keepalive 包含的组件销毁的时候触发
执行时机
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
- beforeEach: 路由全局前置守卫,可用于登录验证、全局路由loading等,router实例上绑定的
- beforeEnter: 路由独享守卫,routerlist上定义的
- beforeRouteEnter: 路由组件的组件进入路由前钩子,vue文件内部定义的
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能访问this。
- created:组件生命周期,可以访问this,不能访问dom。
- beforeMount:组件生命周期
- deactivated: 离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
- mounted:访问/操作dom。
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行beforeRouteEnter回调函数next。
非最佳实践
光伏落地
<keep-alive
ref="keepAlive"
:include="cachedViews"
>
<router-view
v-if="keeplivePathList2.includes($route.path)"
:key="$route.path"
/>
</keep-alive>
<router-view
v-if="!keeplivePathList2.includes($route.path)"
:key="$route.path"
/>
export default {
name: 'AppMain',
data() {
return {
keeplivePathList2: [
'/busManage/getMaterials/list',
'/busManage/customerList',
'/internal/staffManage/list',
'/busManage/site_survey/list',
...
],
}
},
computed: {
cachedViews() {
// .map((item) => item.replace(///g, ''))
return this.$store.state.tagsView.cachedViews
},
},
created() {
},
}
通过v-if方式来确认哪些页面需要增加缓存。PS:这里最好的解决方式是通过在router列表的meta上面加上缓存标签,我们这里router路由在后台管理,后台说是不太好改结构,所以缓存页面在前端写死了。
改造后存在的问题:
1.增加缓存的页面第二次不会执行created钩子函数,所以如果我们在详情页修改了当前数据状态,返回列表时,数据也不会刷新,这里我们
解决办法:将created钩子的执行内容全部移入到activated钩子里,保证列表数据是最新且其他查询、分页参数缓存。
2.关闭当前tags,再次打开页面时,页面仍然缓存关闭之前状态,明显不符合使用习惯。
解决办法:通过include参数来控制,只对当前tags时打开的页面进行缓存,关闭当前tag时,此页面缓存清除,这里一定要注意我们这里绑定的cachedViews路由的path所以我们需要将页面组件的name名称替换成当前页面的路由
光伏新增缓存路由步骤
1.在AppMain.vue页面keeplivePathList2增加需要缓存的路由path
2.将当前路由对应的vue文件的name字段修改成path路径
c端做法
最终实现:home->list 不缓存list
detail->list 缓存list
meta 属性
<keep-alive>
// 这个v-if一定写在router-view上 不要写在keep-alive上
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: home,
meta: {
keepAlive: false // 不需要缓存
}
},
{
path: '/list',
name: 'list',
component: list,
meta: {
keepAlive: true // 需要被缓存
}
},
{
path: '/detail',
name: 'detail',
component: detail,
meta: {
keepAlive: false // 不需要被缓存
}
},
]
})
问题:home->list 也会被缓存
解决办法
home组件
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
if(to.name === 'list') {
to.meta.keepAlive = false; // 让 list 不缓存,即刷新
}
next();
}
};
detail组件
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
if(to.name === 'list') {
to.meta.keepAlive = true; // 让 list 缓存,即不刷新
}
next();
}
};
总结:在跳转前的组件里的beforeRouteLeave钩子中操作to.meta.keepAlive来控制页面是否缓存
用 include (exclude例子类似)
<keep-alive :include="$store.state.keepAlive.list">
<router-view />
</keep-alive>
const state = {
list: []
}
const mutations = {
add(state, name) {
state.list.indexOf(name) < 0 && state.list.push(name)
},
remove(state, name) {
state.list = state.list.filter(v => {
return v != name
})
},
clean(state) {
state.list = []
}
}
export default {
namespaced: true,
state,
mutations
}
list组件
beforeRouteEnter(to, from, next) {
next(vm => {
vm.$store.commit('keepAlive/add', 'List')
})
},
// 页面离开前
beforeRouteLeave(to, from, next) {
// 不是去deital页面就移除
if (['detail'].indexOf(to.name) < 0) {
this.$store.commit('keepAlive/remove', 'List')
}
next()
}
源码解析
位置: src/core/components/keep-alive.js
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)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)
const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
/* 获取组件的key值 */
const key = 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
/* 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
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
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}