keep-alive

有时程序员不希望组件被重新渲染影响使用体验,或出于性能考虑,避免多次重复渲染降低性能,而是希望组件被缓存下来,维持状态。

此时可以使用keep-alive,开启keep-alive后组件的生命周期变为:

  1. 初次进入时:onMounted→onActivated
  2. 退出是:deactivated
  3. 再次进入:只触发onActivated

所以在keep-alive组件中使用钩子函数时,只执行一次的逻辑放在onMounted中,每次进入组件都要执行的方法放在onActivated中。

A.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<template>
<el-card>
<h1>我是A组件</h1>
<el-form :model="form" label-width="120px">
<el-form-item label="Activity name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Activity zone">
<el-select v-model="form.region" placeholder="please select your zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity time">
<el-col :span="11">
<el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%" />
</el-col>
<el-col :span="2" class="text-center">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-time-picker v-model="form.date2" placeholder="Pick a time" style="width: 100%" />
</el-col>
</el-form-item>
<el-form-item label="Instant delivery">
<el-switch v-model="form.delivery" />
</el-form-item>
<el-form-item label="Activity type">
<el-checkbox-group v-model="form.type">
<el-checkbox label="Online activities" name="type" />
<el-checkbox label="Promotion activities" name="type" />
<el-checkbox label="Offline activities" name="type" />
<el-checkbox label="Simple brand exposure" name="type" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="Resources">
<el-radio-group v-model="form.resource">
<el-radio label="Sponsor" />
<el-radio label="Venue" />
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form">
<el-input v-model="form.desc" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">Create</el-button>
<el-button>Cancel</el-button>
</el-form-item>
</el-form>
</el-card>
</template>

<script setup lang='ts'>
import { onMounted, onUnmounted, onActivated, onDeactivated, reactive } from 'vue'

const form = reactive({
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
})

const onSubmit = () => {
console.log('submit!')
}

onMounted(() => {
console.log("mounted")
})
onActivated(()=> {
console.log("activated, keep-alive初始化")
})
onDeactivated(()=> {
console.log("deactivated, keep-alive卸载")
})
onUnmounted(()=> {
console.log("unmounted")
})
</script>

B.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div>
<el-card>
<h1>我是B组件</h1>
<el-transfer v-model="value" :data="data" />
</el-card>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Option {
key: number
label: string
disabled: boolean
}

const generateData = () => {
const data: Option[] = []
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `Option ${i}`,
disabled: i % 4 === 0,
})
}
return data
}

const data = ref<Option[]>(generateData())
const value = ref([])
</script>

App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<el-button type="primary" @click="flag = !flag">切换组件</el-button>
<!-- keep-alive可以为组件做缓存,并保存状态 -->
<!-- 如果只想缓存部分组件,使用include,传入一个需缓存组件名的数组 -->
<!-- 如果不想缓存某些组件,使用exclude,传入一个不缓存组件名的数组 -->
<!-- 如果需要指定最大缓存量,使用max -->
<keep-alive :include="['A']" :exclude="['B']" :max="10">
<A v-if="flag"></A>
<B v-else></B>
</keep-alive>
</template>

<script setup lang="ts">
import { ref } from "vue"
import A from "@/components/A.vue"
import B from "@/components/B.vue"

const flag = ref<boolean>(true)
</script>

源码

在Vue源码(/package/runtime-core/src/components/KeepAlive.ts)中可以看到keep-alive的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// keep-alive的缓存机制
const cache: Cache = new Map()
const keys: Keys = new Set()

const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,

// Marker for special handling inside the renderer. We are not using a ===
// check directly on KeepAlive in the renderer, because importing it directly
// would prevent it from being tree-shaken.
__isKeepAlive: true,

props: {
include: [String, RegExp, Array], // 只有名称匹配的组件会被缓存
exclude: [String, RegExp, Array], // 任何名称匹配的组件都不被缓存
max: [String, Number] // 最多可以缓存多少组件实例
},

setup(props: KeepAliveProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
// KeepAlive communicates with the instantiated renderer via the
// ctx where the renderer passes in its internals,
// and the KeepAlive instance exposes activate/deactivate implementations.
// The whole point of this is to avoid importing KeepAlive directly in the
// renderer to facilitate tree-shaking.
const sharedContext = instance.ctx as KeepAliveContext

// if the internal renderer is not registered, it indicates that this is server-side rendering,
// for KeepAlive, we just need to render its children
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}

const cache: Cache = new Map()
const keys: Keys = new Set()
let current: VNode | null = null

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
;(instance as any).__v_cache = cache
}

const parentSuspense = instance.suspense

const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
// 隐藏容器
const storageContainer = createElement('div')

// 在实例上注册activate和deactivated两个钩子
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
// props可能发生变化,所以patch
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
// 异步队列,patch完成后,执行子节点的activate和deactivated
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
invokeArrayFns(instance.a)
}
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
}

sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
// 将组件移动到隐藏容器中
// 并非真正卸载组件,而是调用move方法,将组件搬运到一个隐藏容器中
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
if (instance.da) {
invokeArrayFns(instance.da)
}
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true
}, parentSuspense)

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
}

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) {
// current active instance should no longer be kept-alive.
// we can't unmount it now but it might be later, so reset its flag now.
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}

// prune cache on include/exclude prop change
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
// props是响应式的,所以需要监听它的值,监听到变化时,这个操作再做一遍
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => !matches(exclude, name))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)

// cache sub tree after render
// 挂载完后才会对pendingCacheKey进行赋值
let pendingCacheKey: CacheKey | null = null
// 缓存函数
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
// pendingCacheKey不为null时,把要缓存的组件存到缓存map里(第一次不执行)
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
// 首先在组件的onMounted或onUpdated钩子中设置缓存
// 因为pendingCacheKey是在keep-alive的render函数中赋值,所以首次不缓存
// 当render完成后,pendingCacheKey就会赋值
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type && cached.key === vnode.key) {
// current instance will be unmounted as part of keep-alive's unmount
resetShapeFlag(vnode)
// but invoke its deactivated hook here
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})

// 返回一个渲染函数
return () => {
pendingCacheKey = null

if (!slots.default) {
return null
}

// 读取插槽的子节点,只渲染单个组件,如果多了会报错(所以不能用v-show)
const children = slots.default()
const rawVNode = children[0]
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
} else if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
// 最后返回的还是组件本身,keep-alive只是一个抽象组件,本身不会被渲染
return rawVNode
}

let vnode = getInnerChild(rawVNode)
const comp = vnode.type as ConcreteComponent

// for async components, name check should be based in its loaded
// inner component if available
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp
)

const { include, exclude, max } = props

// 如果include不包含子组件名称,或exclude包含组件名称,就不缓存,直接返回
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
current = vnode
return rawVNode
}

const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)

// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {
vnode = cloneVNode(vnode)
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
// #1513 it's possible for the returned vnode to be cloned due to attr
// fallthrough or scopeId, so the vnode here may not be the final vnode
// that is mounted. Instead of caching it directly, we store the pending
// key and cache `instance.subTree` (the normalized vnode) in
// beforeMount/beforeUpdate hooks.
// Vnode的key作为缓存的key
pendingCacheKey = key

// KeepAlive组件返回的函数中根据vnode对象的key去缓存中查找是否有已缓存的组件
// 如果有缓存,继承组件实例,将用于描述组件的vnode对象标记为COMPONENT_KEPT_ALIVE
// 这样渲染器就不会重新创建新的组件实例
// 如果缓存不存在,则将vnode对象的key添加到keys集合中
if (cachedVNode) {
// copy over mounted state
// 已被缓存
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 不用创建组件实例,继承缓存的组件即可
if (vnode.transition) {
// recursively update transition hooks on subTree
// 如果组件上有动画,处理动画
setTransitionHooks(vnode, vnode.transition!)
}
// avoid vnode being mounted as fresh
// 标记vnode不会重新渲染
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
// key保持最新的
keys.delete(key)
keys.add(key)
} else {
// 将vnode的key添加到keys中,keys集合用户维护缓存组件的key
keys.add(key)
// prune oldest entry
// LRU算法(最近最少使用页面置换算法),把不活跃的key剔除
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// avoid vnode being unmounted
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
}
}