一、keep-alive基本概念
keep-alive是Vue的内置组件,用于缓存组件实例,避免重复渲染。它具有以下特点:
- 抽象组件:自身不会渲染DOM,也不会出现在父组件链中
- 包裹动态组件:缓存不活动的组件实例,而不是销毁它们
- 生命周期:提供activated和deactivated钩子函数
二、keep-alive核心实现原理
1. 基本工作流程
- 判断当前组件是否需要缓存
- 生成组件唯一key
- 缓存组件实例
- 在被包裹组件上触发对应的生命周期钩子
2. 缓存策略
采用LRU(Least Recently Used)算法:
- 设置最大缓存数量(max属性)
- 优先删除最久未使用的组件
- 新组件加入时,若达到上限则删除最旧组件
三、Vue2实现原理
js
// Vue2 中 keep-alive 的核心实现
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 = [] // 缓存key数组
},
destroyed () {
// 销毁所有缓存实例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key)
}
},
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.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name = getComponentName(componentOptions)
const { include, exclude } = this
// 判断是否需要缓存
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 命中缓存
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // 调整key顺序
} else {
cache[key] = vnode // 缓存组件
keys.push(key)
// 超过max限制时清理最久未使用的组件
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0])
keys.shift()
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
四、Vue3实现原理
js
// Vue3 中 keep-alive 的核心实现
export const KeepAliveImpl = {
name: 'KeepAlive',
__isKeepAlive: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props, { slots }) {
const cache = new Map() // 使用Map存储缓存
const keys = new Set() // 使用Set存储keys
const current = getCurrentInstance()
// 缓存子树
const cacheSubtree = () => {
if (current.subTree) {
cache.set(current.subTree.key, current.subTree)
keys.add(current.subTree.key)
}
}
// 修剪缓存
const pruneCache = (filter?: (name: string) => boolean) => {
cache.forEach((vnode, key) => {
const name = vnode.type.name
if (name && (!filter || filter(name))) {
pruneCacheEntry(key)
}
})
}
// 清理缓存条目
const pruneCacheEntry = (key: CacheKey) => {
const cached = cache.get(key)
if (!current || !isSameVNodeType(cached, current)) {
unmount(cached)
}
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))
}
)
// 卸载时清理所有缓存
onBeforeUnmount(() => {
cache.forEach(cached => {
unmount(cached)
})
})
return () => {
const children = slots.default?.()
if (!children) return null
const vnode = children[0]
if (!vnode) return null
const comp = vnode.type
const name = comp.name
// 检查是否应该缓存
if (
(props.include && (!name || !matches(props.include, name))) ||
(props.exclude && name && matches(props.exclude, name))
) {
return vnode
}
const key = vnode.key == null ? comp : vnode.key
const cached = cache.get(key)
// 命中缓存
if (cached) {
vnode.el = cached.el
vnode.component = cached.component
// 标记为kept-alive
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
// 缓存新组件
cache.set(key, vnode)
keys.add(key)
// 超过max限制时清理
if (props.max && cache.size > parseInt(props.max)) {
pruneCacheEntry(keys.values().next().value)
}
}
// 标记keepAlive
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
return vnode
}
}
}
五、Vue2和Vue3实现差异对比
1. 数据结构
-
Vue2: 使用普通对象和数组存储缓存
jsthis.cache = Object.create(null) this.keys = []
-
Vue3: 使用Map和Set存储缓存
jsconst cache = new Map() const keys = new Set()
2. 组件实现方式
- Vue2: 选项式API,通过created、mounted等生命周期实现
- Vue3: 组合式API,使用setup函数实现,逻辑更集中
3. 渲染机制
- Vue2: 在render函数中直接操作VNode
- Vue3: 使用新的渲染器架构,更好地支持Fragment和异步组件
4. 性能优化
- Vue3优势:
- 更高效的响应式系统
- 更智能的编译优化
- 更好的Tree-shaking支持
- 更完善的TypeScript支持
5. 生命周期钩子
-
Vue2:
jsexport default { activated() {}, deactivated() {} }
-
Vue3:
jsimport { onActivated, onDeactivated } from 'vue' setup() { onActivated(() => {}) onDeactivated(() => {}) }
六、使用方法案例
1. Vue2中的使用方法
基础用法
html
<!-- App.vue -->
<template>
<div id="app">
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
</div>
</template>
<script>
import ComponentA from './components/ComponentA.vue'
import ComponentB from './components/ComponentB.vue'
export default {
name: 'App',
components: {
ComponentA,
ComponentB
},
data() {
return {
currentComponent: 'ComponentA'
}
}
}
</script>
配合路由使用
js
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/list',
component: () => import('./views/List.vue'),
meta: {
keepAlive: true // 需要缓存的路由
}
},
{
path: '/detail',
component: () => import('./views/Detail.vue'),
meta: {
keepAlive: false // 不需要缓存的路由
}
}
]
export default new VueRouter({
routes
})
html
<!-- App.vue -->
<template>
<div id="app">
<!-- 缓存路由组件 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不缓存的路由组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
</template>
使用include和exclude
html
<template>
<div id="app">
<keep-alive :include="['ComponentA', 'ComponentB']" :exclude="['ComponentC']">
<router-view></router-view>
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
2. Vue3中的使用方法
基础用法
html
<!-- App.vue -->
<template>
<div id="app">
<KeepAlive>
<component :is="currentComponent"></component>
</KeepAlive>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ComponentA from './components/ComponentA.vue'
import ComponentB from './components/ComponentB.vue'
const currentComponent = ref('ComponentA')
</script>
配合路由使用
ts
// router.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/list',
component: () => import('./views/List.vue'),
meta: {
keepAlive: true
}
},
{
path: '/detail',
component: () => import('./views/Detail.vue'),
meta: {
keepAlive: false
}
}
]
export default createRouter({
history: createWebHistory(),
routes
})
html
<!-- App.vue -->
<template>
<div id="app">
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" v-if="$route.meta.keepAlive" />
</KeepAlive>
<component :is="Component" v-if="!$route.meta.keepAlive" />
</RouterView>
</div>
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>
使用include和exclude
html
<!-- App.vue -->
<template>
<div id="app">
<RouterView v-slot="{ Component }">
<KeepAlive :include="['ListPage']" :max="10">
<component :is="Component" />
</KeepAlive>
</RouterView>
</div>
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>
在组件中使用生命周期钩子
html
<!-- ListPage.vue -->
<template>
<div class="list-page">
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, onActivated, onDeactivated } from 'vue'
const list = ref([])
// 在组件被激活时触发
onActivated(() => {
console.log('组件被激活')
// 可以在这里恢复组件的状态,如滚动位置
})
// 在组件被停用时触发
onDeactivated(() => {
console.log('组件被停用')
// 可以在这里保存组件的状态
})
</script>
七、总结
-
核心原理相同:
- 都使用LRU缓存策略
- 都支持include/exclude过滤
- 都实现了组件缓存和重用
-
主要改进:
- Vue3使用更现代的数据结构
- 更清晰的代码组织方式
- 更好的性能优化
- 更强大的TypeScript支持
- 更完善的错误处理机制