Appearance
37.keep-alive 平时在哪里使用?
1.1 概念
keep-alive 是 vue 中的内置组件,能在组件切换过程会缓存组件的实例,而不是销毁它们。在组件再次重新激活时可以通过缓存的实例拿到之前渲染的 DOM 进行渲染,无需重新生成节点。
1.2 使用场景
动态组件可以采用keep-alive
进行缓存
vue
<template>
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<component :is="component"></component>
</keep-alive>
</template>
<template>
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<component :is="component"></component>
</keep-alive>
</template>
在路由中使用 keep-alive
vue
<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>
<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 属性指定哪些页面需要缓存,哪些不需要
vue
<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>
<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>
1.3 原理
1).vue2 原理
js
export default {
name: 'keep-alive',
abstract: true, // 不会放到对应的lifecycle
props: {
include: patternTypes, // 白名单
exclude: patternTypes, // 黑名单
max: [String, Number] // 缓存的最大个数
},
created () {
this.cache = Object.create(null) // 缓存列表
this.keys = [] // 缓存的key列表
},
destroyed () {
for (const key in this.cache) { // keep-alive销毁时 删除所有缓存
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: VNode = getFirstComponentChild(slot) 、// 获得第一个组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = 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
if (cache[key]) { // 如果有key 将组件实例直接复用
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // lru算法
} else {
cache[key] = vnode // 缓存组件
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个
}
}
vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性
}
return vnode || (slot && slot[0])
}
}
export default {
name: 'keep-alive',
abstract: true, // 不会放到对应的lifecycle
props: {
include: patternTypes, // 白名单
exclude: patternTypes, // 黑名单
max: [String, Number] // 缓存的最大个数
},
created () {
this.cache = Object.create(null) // 缓存列表
this.keys = [] // 缓存的key列表
},
destroyed () {
for (const key in this.cache) { // keep-alive销毁时 删除所有缓存
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: VNode = getFirstComponentChild(slot) 、// 获得第一个组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = 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
if (cache[key]) { // 如果有key 将组件实例直接复用
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // lru算法
} else {
cache[key] = vnode // 缓存组件
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个
}
}
vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性
}
return vnode || (slot && slot[0])
}
}
2).vue3 原理
js
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 = null
const parentSuspense = instance.suspense
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
const 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) {
// reset the shapeFlag so it can be properly unmounted
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 VNode
if (!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))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)
// 在渲染后缓存子树
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
// ...
return () => {
// ...
if (cachedVNode) {
vnode.el = cachedVNode.el // 复用缓存dom
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// 避免 vnode 作为新的组件实例被挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// LRU算法
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value) // 删除缓存中的第一个
}
}
// 避免组件卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
}
}
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 = null
const parentSuspense = instance.suspense
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
const 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) {
// reset the shapeFlag so it can be properly unmounted
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 VNode
if (!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))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)
// 在渲染后缓存子树
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
// ...
return () => {
// ...
if (cachedVNode) {
vnode.el = cachedVNode.el // 复用缓存dom
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// 避免 vnode 作为新的组件实例被挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// LRU算法
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value) // 删除缓存中的第一个
}
}
// 避免组件卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
}
}
核心原理就是缓存 + LRU 算法
1.4 keep-alive 中数据更新问题
beforeRouteEnter:在有 vue-router 的项目,每次进入路由的时候,都会执行beforeRouteEnter
js
beforeRouteEnter(to, from, next){
next(vm=>{
vm.getData() // 获取数据
})
},
beforeRouteEnter(to, from, next){
next(vm=>{
vm.getData() // 获取数据
})
},
actived:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
js
activated(){
this.getData() // 获取数据
},
activated(){
this.getData() // 获取数据
},