一、概念
-
keep-alive 是 Vue.js 中的一个内置组件,它用于缓存组件的状态或避免对组件进行多次销毁和重建。通过使用 keep-alive 组件,可以在组件切换时将状态保留在内存中,以便在下次需要时直接复用,从而提高性能并改善用户体验。
-
具体来说,当一个被 keep-alive 包裹的组件被切换出去时,它的状态会被保留,而不会被销毁。当再次切换回来时,该组件的状态会被恢复,避免了重新渲染和重新初始化。这对于包含大量动态数据、复杂计算或需要长时间初始化的组件来说,可以显著减少页面加载时间和提升用户体验。
-
在 Vue.js 中使用 keep-alive 组件通常需要配合动态组件()或者路由来实现组件的切换和缓存。在实际开发中,常见的应用场景包括标签页切换、路由切换、模态框等需要保持状态的场景。
二、使用场景
<template><keep-alive :include="whiteList" :exclude="blackList" :max="count"><component :is="component"></component></keep-alive>
</template>
在路由中使用 keep-alive
<template><!-- vue2写法 --><keep-alive :include="whiteList" :exclude="blackList" :max="count"><router-view></router-view></keep-alive><!-- vue3写法 --><router-view v-slot="{ Component }"><keep-alive :include="whiteList" :exclude="blackList" :max="count"><component :is="Component" /></keep-alive></router-view>
</template>
也可以通过 meta 属性指定哪些页面需要缓存,哪些不需要
<template><!-- vue2写法 --><keep-alive><!-- 需要缓存的视图组件 --><router-view v-if="$route.meta.keepAlive"></router-view></keep-alive><!-- 不需要缓存的视图组件 --><router-view v-if="!$route.meta.keepAlive"></router-view><!-- vue3写法 --><router-view v-slot="{ Component }"><transition><keep-alive><!-- 需要缓存的视图组件 --><component :is="Component" v-if="route.meta.keepAlive" /></keep-alive><!-- 不需要缓存的视图组件 --><component :is="Component" v-if="!route.meta.keepalive" /></transition></router-view>
</template>
四、底层原理的实现
- vue2 底层原理的实现
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) { // keep-alive组件销毁时,删除所有缓存的组件实例pruneCacheEntry(this.cache, key, this.keys)}},mounted () { // 监控include和exclude属性的变化,根据变化来对缓存进行调整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.defaultconst vnode = getFirstComponentChild(slot) // 获取slot中的第一个组件节点const componentOptions = vnode && vnode.componentOptions // 获取组件的选项if (componentOptions) {const name = getComponentName(componentOptions) // 获取组件名const { include, exclude } = thisif (// 不在白名单中(include && (!name || !matches(include, name))) ||// 在黑名单中(exclude && name && matches(exclude, name))) {return vnode // 不需要缓存的组件直接返回}const { cache, keys } = thisconst key = vnode.key == null? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') // 生成缓存的键: vnode.keyif (cache[key]) { // 如果有缓存,直接复用组件实例vnode.componentInstance = cache[key].componentInstanceremove(keys, key)keys.push(key) // LRU算法,最近使用的组件放在数组末尾} 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 // 在组件VNode的data中增加keep-alive属性,表示该组件已被缓存}return vnode || (slot && slot[0]) // 返回处理后的VNode或原始的slot内容}
}
- vue3 底层原理的实现
const KeepAliveImpl: ComponentOptions = {name: `KeepAlive`,__isKeepAlive: true,props: {include: [String, RegExp, Array], // 指定需要缓存的组件名称规则exclude: [String, RegExp, Array], // 指定不需要缓存的组件名称规则max: [String, Number] // 缓存的最大组件实例数量},setup(props: KeepAliveProps, { slots }: SetupContext) {const instance = getCurrentInstance()! // 获取当前组件实例const sharedContext = instance.ctx as KeepAliveContext // 获取共享上下文// 创建缓存组件实例的 Map 和键集合const cache: Cache = new Map()const keys: Keys = new Set()let current: VNode | null = nullconst parentSuspense = instance.suspenseconst {renderer: {p: patch,m: move,um: _unmount,o: { createElement }}} = sharedContextconst storageContainer = createElement('div') // 创建一个用于存储组件对应DOM的容器// 激活组件时调用的方法sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {const instance = vnode.component!move(vnode, container, anchor, MoveType.ENTER, parentSuspense)// ... 省略部分代码}// 失活组件时调用的方法sharedContext.deactivate = (vnode: VNode) => {const instance = vnode.component!move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)// ... 省略部分代码}// 卸载组件function unmount(vnode: VNode) {// 重置 shapeFlag 以便能够正确卸载resetShapeFlag(vnode)_unmount(vnode, instance, parentSuspense, true)}// 处理缓存function pruneCache(filter?: (name: string) => boolean) {cache.forEach((vnode, key) => {const name = getComponentName(vnode.type as ConcreteComponent)if (name && (!filter || !filter(name))) {pruneCacheEntry(key)}})}// 移除头部缓存function pruneCacheEntry(key: CacheKey) {const cached = cache.get(key) as VNodeif (!current || !isSameVNodeType(cached, current)) {unmount(cached)} else if (current) {resetShapeFlag(current)}cache.delete(key)keys.delete(key)}// 监听 include 和 exclude 属性的变化,然后清理缓存watch(() => [props.include, props.exclude],([include, exclude]) => {include && pruneCache(name => matches(include, name))exclude && pruneCache(name => !matches(exclude, name))},// 在 `current` 更新后进行清理{ flush: 'post', deep: true })// 在渲染后缓存子树let pendingCacheKey: CacheKey | null = nullconst cacheSubtree = () => {if (pendingCacheKey != null) {cache.set(pendingCacheKey, getInnerChild(instance.subTree))}}onMounted(cacheSubtree)onUpdated(cacheSubtree)// ... 省略部分代码return () => {// ... 省略部分代码if (cachedVNode) {vnode.el = cachedVNode.el // 复用缓存的 DOMvnode.component = cachedVNode.componentif (vnode.transition) {// 递归更新子树上的过渡钩子setTransitionHooks(vnode, vnode.transition!)}// 避免 vnode 被作为新的组件实例被挂载vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE// LRU算法keys.delete(key)keys.add(key)} else {keys.add(key)// 移除最旧的缓存if (max && keys.size > parseInt(max as string, 10)) {pruneCacheEntry(keys.values().next().value)}}// 避免组件被卸载vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVEcurrent = vnodereturn isSuspense(rawVNode.type) ? rawVNode : vnode}}
}
小结: 核心原理就是缓存 + LRU 算法
五、keep-alive 中数据更新问题
beforeRouteEnter: 在有 vue-router 的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){next(vm=>{vm.getData() // 获取数据})
},
actived: 在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){this.getData() // 获取数据
},