使用场景:
列表查询出约课记录之后,跳转到详情后再返回页面内容刷新,之前搜索的结果就没有了,为了解决这一问题,使用了keep-alive组件来实现页面缓存。
实现方案:
keep-alive
实现原理:
- keep-alive是vue2.0提供的用来缓存的内置组件,避免多次加载相同的组建,减少性能消耗。(keep-alive.js)
- 将需要缓存的VNode节点保存在this.cache中(而不是直接存储DOM结构),在render时,如果VNode的name符合缓存条件,则直接从this.cache中取出缓存的VNode实例进行渲染。
- keep-alive的渲染是在patch阶段,在已经缓存的情况下不会进入$mount阶段,所以mounted之前的钩子只会执行一次。
keep-alive.js源码:
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断当前组件虚拟dom是否渲染成真实dom的关键
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的组件
},
created() {
this.cache = Object.create(null) // 缓存虚拟dom
this.keys = [] // 缓存的虚拟dom的键集合
},
destroyed() {
for (const key in this.cache) {
// 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matched(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
// 先省略...
}
}
render函数解读
- 通过getFirstComponentChild获取第一个组件(vnode);
- 获取该组件的name(有name的返回name,无name返回标签名);
- 将这个name通过include、exclude属性进行匹配;
- 匹配不成功说明不需要缓存,直接返回vnode;
- 匹配成功后,根据key在this.cache中查找是否已经被缓存过;
- 如果已缓存过,将缓存的VNode的组件实例componentInsance覆盖到当前vnode上,并返回;
- 未被缓存则将VNode存储在this.cache中;
render () {
/* 得到slot插槽中的第一个组件 */
const vnode: VNode = getFirstComponentChild(this.$slots.default)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取组件名称,优先获取组件的name字段,否则是组件的tag */
const name: ?string = getComponentName(componentOptions)
/* name不在inlcude中或者在exlude中则直接返回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
/* 如果已经做过缓存了则直接从缓存中获取组件实例给vnode,还未缓存过则进行缓存 */
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
}
/* keepAlive标记位 */
vnode.data.keepAlive = true
}
return vnode
}
渲染阶段
只执行一次的钩子:当vnode.componentInstance和keepAlive为true时,不再进入$mount过程,也就不会执行mounted以及之前的钩子函数(beforeCreated、created、mounted)
// src/core/vdom/create-component.js
const componentVNodeHooks = {
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)
}
}
// ...
}
可重复执行的activated:在patch阶段,最后会执行invokeInsertHook函数,这个函数调用组件实例的insert钩子,insert钩子中调用了activateChildComponent函数,递归执行子组件中的activated钩子函数
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]) // 调用VNode自身的insert钩子函数
}
}
}
// src/core/vdom/create-component.js
const componentVNodeHooks = {
// init()
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
// ...
}
页面缓存的使用方法:
- keep-alive组件提供了两个属性include、exclude,可以用逗号隔开的字符串或正则表达式、一个数组来表示。
- keep-alive的生命周期函数created、mounted只有创建时会被触发一次
- 生命周期钩子有两个activated、deactivated,分别在组件激活、非激活状态时触发
- 因为keep-alive将组件缓存起来,不会被销毁和重建,所以不会重新调用created、mounted方法。
- 需要重置data数据Object.assign(this.options.data.call(this))
- 从指定路由跳转回来需要刷新的情况,可以结合路由守卫beforeRouterEnter和beforeRouterLeave来区分是否需要刷新
- 使用vue-devtool观察组件的缓存状态,灰色的为被缓存起来的状态
pageList → pageDetail → pageList,pageList保存原有状态;pageList → 其他 → pageList,pageList初始化状态
使用方法一
// 使用router的meta属性控制
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta || !$route.meta.keepAlive" class="main"></router-view>
// router.js
{
path: 'pageList',
name: 'pageList',
component: () => import('../pages/pageList.vue'),
meta: {
// keepAlive是否使用keep-alive组件,isUseCache是否需要缓存
keepAlive: true, isUseCache: false
}
}
// pageList.vue
<template>
<div></div>
</template>
<script>
export default {
name: 'pageList',
data() {
return {}
},
activated() {
// 组件活动状态
if (!this.$route.meta.isUseCache) {
// 不需要缓存时需要重置数据
Object.assign(this.$data, this.$options.data.call(this))
// 设置为需要缓存
this.$route.meta.isUseCache = true
}
},
deactivated() {
// 组件非活跃状态
},
beforeRouteEnter(to, from, next) {
// 从pageA页面跳转到pageB需要缓存,其他页面跳转会pageB不需要缓存
if(from.name != 'pageDetail'){
to.meta.isUseCache = false;
} else {
to.meta.isUseCache = true
}
},
}
</script>
pageList → pageDetail → pageList,pageList保存原有状态;pageList → 其他 → pageList,pageList初始化状态
使用方法二:
// 使用keep-alive的prop属性
// cacheList可以存储在vuex中,默认为'pageList'
<keep-alive :include="cacheList">
<router-view></router-view>
</keep-alive>
// pageList.vue
beforeRouteLeave (to, from, next) {
if (to.name !== 'pageDetail') {
this.$store.dispatch('setCacheList', '')
}else {
this.$store.dispatch('setCacheList', 'pageList')
}
next()
}
问题:方法二中,pageList → pageDetail → 其他 → pageList,pageList采用了缓存的数据, 解决:其他页面中的beforeRouteLeave增加 this.$store.dispatch('setCacheList', '')