前言
Vue3探秘系列文章链接:
不止响应式:Vue3探秘系列--- 虚拟结点vnode的页面挂载之旅(一)
不止响应式:Vue3探秘系列--- 组件更新会发生什么(二)
Hello~大家好。我是秋天的一阵风
在 Vue.js
中,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态------当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
在下面的例子中,你会看到两个有状态的组件------A 有一个计数器,而 B 有一个通过 v-model
同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:
你会发现在切回来之后,之前已更改的状态都被重置了。
在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被"切走"的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive>
内置组件将这些动态组件包装起来:
javascript
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
现在,在组件切换时状态也能被保留了:你可以点击这里查看在线效果
一、keep-alive的几个参数
如果你对
keep-alive
的使用已经非常熟悉,可以跳过这一节。
1. 包含与排除
<KeepAlive>
默认会缓存内部的所有组件实例,但我们可以通过 include
和 exclude
prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
javascript
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
它会根据组件的 name
选项进行匹配,所以组件如果想要条件性地被 KeepAlive
缓存,就必须显式声明一个 name
选项。
2. 最大缓存数
我们可以通过传入 max
prop 来限制可被缓存的最大组件实例数。<KeepAlive>
的行为在指定了 max
后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间
javascript
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
3. 生命周期
当一个组件实例从 DOM 上移除但因为被 <KeepAlive>
缓存而仍作为组件树的一部分时,它将变为不活跃 状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
一个持续存在的组件可以通过 onActivated()
和 onDeactivated()
注册相应的两个状态的生命周期钩子:
javascript
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
请注意:
onActivated
在组件挂载时也会调用,并且onDeactivated
在组件卸载时也会调用。- 这两个钩子不仅适用于
<KeepAlive>
缓存的根组件,也适用于缓存树中的后代组件。
二、keep-alive的实现原理
为了更好的理解,我们先看一个示例代码:
javascript
<keep-alive>
<comp-a v-if="flag"></comp-a>
<comp-b v-else></comp-b>
<button @click="flag=!flag">toggle</button>
</keep-alive>
可以看到,点击按钮时,flag
变量的值会变化,并且comp-a
和comp-b
组件会根据这个变量进行切换
我们可以用模板导出工具看一下它编译后的 render
函数:
javascript
import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_comp_a = _resolveComponent("comp-a")
const _component_comp_b = _resolveComponent("comp-b")
return (_openBlock(), _createBlock(_KeepAlive, null, [
(_ctx.flag)
? _createVNode(_component_comp_a, { key: 0 })
: _createVNode(_component_comp_b, { key: 1 }),
_createVNode("button", {
onClick: $event => (_ctx.flag=!_ctx.flag)
}, "toggle", 8 /* PROPS */, ["onClick"])
], 1024 /* DYNAMIC_SLOTS */))
}
我们使用了 KeepAlive
组件对这两个组件做了一层封装,KeepAlive 是一个抽象组件,它并不会渲染成一个真实的 DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载 DOM的流程,从而优化了性能。
1. KeepAlive 组件的定义
javascript
const KeepAliveImpl = {
name: `KeepAlive`,
__isKeepAlive: true,
inheritRef: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props, { slots }) {
const cache = new Map()
const keys = new Set()
let current = null
const instance = getCurrentInstance()
const parentSuspense = instance.suspense
const sharedContext = instance.ctx
const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext
const storageContainer = createElement('div')
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component
move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)
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)
}
sharedContext.deactivate = (vnode) => {
const instance = vnode.component
move(vnode, storageContainer, null, 1 /* 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)
}
function unmount(vnode) {
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense)
}
function pruneCache(filter) {
cache.forEach((vnode, key) => {
const name = getName(vnode.type)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
function pruneCacheEntry(key) {
const cached = cache.get(key)
if (!current || cached.type !== current.type) {
unmount(cached)
}
else if (current) {
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
watch(() => [props.include, props.exclude], ([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && !pruneCache(name => matches(exclude, name))
})
let pendingCacheKey = null
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, instance.subTree)
}
}
onBeforeMount(cacheSubtree)
onBeforeUpdate(cacheSubtree)
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
if (cached.type === subTree.type) {
resetShapeFlag(subTree)
const da = subTree.component.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})
return () => {
pendingCacheKey = null
if (!slots.default) {
return null
}
const children = slots.default()
let vnode = children[0]
if (children.length > 1) {
if ((process.env.NODE_ENV !== 'production')) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
}
else if (!isVNode(vnode) ||
!(vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */)) {
current = null
return vnode
}
const comp = vnode.type
const name = getName(comp)
const { include, exclude, max } = props
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) {
return (current = vnode)
}
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
if (vnode.el) {
vnode = cloneVNode(vnode)
}
pendingCacheKey = key
if (cachedVNode) {
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
keys.delete(key)
keys.add(key)
}
else {
keys.add(key)
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
current = vnode
return vnode
}
}
}
我们将keep-alive
拆分为四个部分,分别是组件渲染 ,缓存 ,props ,卸载。
(1)组件的渲染
我们之前在探究setup
的章节时知道,setup
函数的返回值可以是对象 ,也可以是函数。
如果是函数那么这个函数就是组件的render
函数:
javascript
return () => {
pendingCacheKey = null
if (!slots.default) {
return null
}
const children = slots.default()
let vnode = children[0]
if (children.length > 1) {
if ((process.env.NODE_ENV !== 'production')) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
}
else if (!isVNode(vnode) ||
!(vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */)) {
current = null
return vnode
}
const comp = vnode.type
const name = getName(comp)
const { include, exclude, max } = props
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) {
return (current = vnode)
}
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
if (vnode.el) {
vnode = cloneVNode(vnode)
}
pendingCacheKey = key
if (cachedVNode) {
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 避免 vnode 节点作为新节点被挂载
vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
// 让这个 key 始终新鲜
keys.delete(key)
keys.add(key)
}
else {
keys.add(key)
// 删除最久不用的 key,符合 LRU 思想
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// 避免 vnode 被卸载
vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
current = vnode
return vnode
}
-
首先会从
slots.default
拿到包裹的子节点children
,判断如果长度大于1
就会报出警告只能包含一个确切的组件 -
如果不考虑缓存的情况下,
return
出去的vnode
其实就是children
数组第一个子节点 -
我们在开头提到过一句:
keep-alive
是一个抽象组件 ,原因就是keep-alive
只负责渲染子节点,它自己本身没有具体的实体内容。
(2)缓存的设计
我们先思考一下,如果让你来实现缓存,该如何实现呢?
换个思路来说,我们是不是可以把缓存理解成:不需要重复渲染。
如果在渲染Dom
的时候,我们做一些Dom
的缓存处理,在下一次渲染的时候把之前缓存的Dom
取出来渲染是不是就能实现了呢?
我们来关注下 KeepAlive
组件的生命周期,它注入了两个钩子函数,onBeforeMount
和 onBeforeUpdate
,在这两个钩子函数内部都执行了 cacheSubtree
函数来做缓存:
javascript
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, instance.subTree)
}
}
在第一次渲染之前时会执行onBeforeMount
,pendingCacheKey
还没来得及被赋值,所以第一次渲染的组件并不会被缓存。
第一次执行完setup
的render
函数以后,这个时候pendingCacheKey
才会被赋值。
这个pendingCacheKey
的值是vnode
身上的属性key
,你可能会疑惑,这个key
啥时候添加上的?
我们可以看看它的渲染模板:
javascript
import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_comp_a = _resolveComponent("comp-a")
const _component_comp_b = _resolveComponent("comp-b")
return (_openBlock(), _createBlock(_KeepAlive, null, [
(_ctx.flag)
? _createVNode(_component_comp_a, { key: 0 })
: _createVNode(_component_comp_b, { key: 1 }),
_createVNode("button", {
onClick: $event => (_ctx.flag=!_ctx.flag)
}, "toggle", 8 /* PROPS */, ["onClick"])
], 1024 /* DYNAMIC_SLOTS */))
}
我们注意到KeepAlive
的子节点创建的时候都添加了一个 key
的 prop
,它就是专门为 KeepAlive
的缓存设计的,这样每一个子节点都能有一个唯一的 key
。
缓存流程
- 页面首先渲染
A组件
,接着当我们点击按钮的时候,修改了flag
的值,会触发当前组件的重新渲染,进而也触发了KeepAlvie
组件的重新渲染,在组件重新渲染前,会执行onBeforeUpdate
对应的钩子函数,也就再次执行到cacheSubtree
函数中。
这个时候 pendingCacheKey
对应的是 A 组件 vnode
的 key
,instance.subTree
对应的也是 A
组件的渲染子树,所以 KeepAlive
每次在更新前,会缓存前一个组件的渲染子树。
-
这个时候渲染了
B组件
,当我们再次点击按钮,修改flag
值的时候,会再次触发KeepAlvie
组件的重新渲染,当然此时执行onBeforeUpdate
钩子函数缓存的就是B组件
的渲染子树了。 -
接着再次执行
KeepAlive
组件的render
函数,此时就可以从缓存中根据A
组件的key
拿到对应的渲染子树cachedVNode
的了,然后执行如下逻辑:
javascript
if (cachedVNode) {
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 避免 vnode 节点作为新节点被挂载
vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
// 让这个 key 始终新鲜
keys.delete(key)
keys.add(key)
}
else {
keys.add(key)
// 删除最久不用的 key,符合 LRU 思想
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM
以及组件实例 component
,赋值给KeepAlive
的 vnode
,并更新 vnode.shapeFlag
,以便后续 patch
阶段使用。
patch函数
虽然拿到了缓存的组件实例并且已经赋值,但是这还没完全实现缓存功能,我们还是得请出我们的老演员 patch 函数
,看看组件渲染的时候有keep-alive
缓存和没有缓存有什么区别。
patch函数会进入到
processComponent
分支,所以我们直接从processComponent
开始看
javascript
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 处理 KeepAlive 组件
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)
}
else {
// 挂载组件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
else {
// 更新组件
}
}
KeepAlive
首次渲染某一个子节点时,和正常的组件节点渲染没有区别,但是有缓存后,由于标记了 shapeFlag
,所以在执行processComponent
函数时会走到处理 KeepAlive
组件的逻辑中,执行 KeepAlive
组件实例上下文中的 activate
函数,我们来看它的实现:
activate 函数
javascript
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component
move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)
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)
}
可以看到,由于此时已经能从 vnode.el
中拿到缓存的 DOM
了,所以可以直接调用move
方法挂载节点,然后执行 patch
方法更新组件,以防止 props
发生变化的情况。
接下来,就是通过 queuePostRenderEffect
的方式,在组件渲染完毕后,执行子节点组件定义的 activated
钩子函数。
至此,我们就了解了 KeepAlive
的缓存设计,KeepAlive
包裹的子组件在其渲染后,下一次 KeepAlive
组件更新前会被缓存,缓存后的子组件在下一次渲染的时候直接从缓存中拿到子树vnode
以及对应的DOM
元素,直接渲染即可。
(3)Props 设计
KeepAlive 一共支持了三个 Props
,分别是 include、exclude 和 max
javascript
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
}
include、exclude : 包含与排除
javascript
const { include, exclude, max } = props
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) {
return (current = vnode)
}
很好理解,如果子组件名称不匹配 include
的 vnode
,以及子组件名称匹配 exclude
的 vnode
都不应该被缓存,而应该直接返回。
当然,由于props
是响应式的,在 include
和 exclude
props 发生变化的时候也应该有相关的处理逻辑,如下:
javascript
watch(() => [props.include, props.exclude], ([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && !pruneCache(name => matches(exclude, name))
})
监听的逻辑也很简单,当 include
发生变化的时候,从缓存中删除那些name
不匹配 include
的 vnode 节点
;
当 exclude
发生变化的时候,从缓存中删除那些name
匹配 exclude
的 vnode
节点。
max: 最大缓存数
KeepAlive
还提供一个max
参数让我们可以限制缓存的个数,因为缓存本身就是占用内存的,无限制的缓存可能会导致性能问题。
javascript
keys.add(key)
// 删除最久不用的 key,符合 LRU 思想
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
由于新的缓存 key
都是在 keys
的结尾添加的,所以当缓存的个数超过 max
的时候,就从最前面开始删除,符合 LRU
最近最少使用的算法思想。关于LRU,我们会在下一个章节会具体介绍。
(4)组件的卸载
前面我们提到 KeepAlive 渲染的过程实际上是渲染它的第一个子组件节点,并且会给渲染的 vnode 打上如下标记: vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
加上这个 shapeFlag
有什么用呢,我们结合前面的示例来分析。
javascript
<keep-alive>
<comp-a v-if="flag"></comp-a>
<comp-b v-else></comp-b>
<button @click="flag=!flag">toggle</button>
</keep-alive>
当 flag
为 true
的时候,渲染 A 组件
,然后我们点击按钮修改 flag
的值,会触发KeepAlive
组件的重新渲染,会先执行 BeforeUpdate
钩子函数缓存 A 组件
对应的渲染子树 vnode
,然后再执行 patch
更新子组件。
这个时候会执行 B 组件
的渲染,以及 A 组件
的卸载,我们知道组件的卸载会执行 unmount
方法,其中有一个关于 KeepAlive
组件的逻辑,如下:
deactivate
javascript
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {
const { shapeFlag } = vnode
if (shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
parentComponent.ctx.deactivate(vnode)
return
}
// 卸载组件
}
如果 shapeFlag
满足 KeepAlive
的条件,则执行相应的deactivate
函数,它的定义如下
javascript
sharedContext.deactivate = (vnode) => {
const instance = vnode.component
move(vnode, storageContainer, null, 1 /* 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)
}
函数首先通过 move
方法从 DOM
树中移除该节点,接着通过 queuePostRenderEffect
的方式执行定义的 deactivated
钩子函数。
注意,这里我们只是移除了 DOM,并没有真正意义上的执行子组件的整套卸载流程。
那么除了点击按钮引起子组件的卸载之外,当 KeepAlive
所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive
组件的卸载,在卸载的过程中会执行 onBeforeUnmount
钩子函数,如下:
javascript
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
if (cached.type === subTree.type) {
resetShapeFlag(subTree)
const da = subTree.component.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})
它会遍历所有缓存的 vnode
,并且比对缓存的 vnode
是不是当前 KeepAlive
组件渲染的 vnode
。
如果是的话,则执行 resetShapeFlag
方法,它的作用是修改vnode
的 shapeFlag
,不让它再被当作一个 KeepAlive
的 vnode
了,这样就可以走正常的卸载逻辑。
接着通过 queuePostRenderEffect
的方式执行子组件的 deactivated
钩子函数。
如果不是,则执行 unmount
方法重置 shapeFlag
以及执行缓存 vnode
的整套卸载流程。
三、LRU算法
LRU(Least Recently Used)
算法是一种常用的缓存淘汰策略,用于在缓存空间有限的情况下,根据数据项的访问时间顺序来决定哪些数据项应该被优先淘汰。在 Vue.js 中,keep-alive
组件就是利用 LRU 算法来管理缓存的组件实例。
1. LRU 缓存的基本原理
LRU 算法的核心在于维护一个有序的数据结构,用来记录数据项的访问顺序。当缓存达到最大容量时,需要淘汰最久未被访问的数据项。常见的数据结构选择包括双向链表结合哈希表
2. 使用 JavaScript 实现 LRU 缓存
下面是一个使用 JavaScript 实现的 LRU 缓存示例:
javascript
class LRUCache {
constructor(capacity) {
// 初始化缓存容量
this.capacity = capacity;
// 使用 Map 存储键值对
this.map = new Map();
// 使用数组记录访问顺序,数组尾部是最近使用的项
this.keys = [];
}
get(key) {
// 查找键是否存在于缓存中
const index = this.keys.indexOf(key);
if (index === -1) return -1; // 如果不存在,返回 -1
// 将访问过的键移动到数组的末尾,表示它是最近使用的
this.keys.splice(index, 1);
this.keys.push(key);
// 返回键对应的值
return this.map.get(key);
}
put(key, value) {
// 如果键已经存在,则更新其值,并将其移动到数组末尾
if (this.map.has(key)) {
this.map.set(key, value);
const index = this.keys.indexOf(key);
this.keys.splice(index, 1);
this.keys.push(key);
} else {
// 如果缓存已满,则移除最久未使用的项
if (this.keys.length >= this.capacity) {
const lruKey = this.keys.shift(); // 移除数组头部的键
this.map.delete(lruKey); // 从 Map 中删除该键
}
// 添加新的键值对
this.map.set(key, value);
this.keys.push(key); // 将新键添加到数组末尾
}
}
}
// 示例使用
const cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
console.log(cache.get(1)); // 返回 1
cache.put(3, 3); // 移除键 2
console.log(cache.get(2)); // 返回 -1 (未找到)
cache.put(4, 4); // 移除键 1
console.log(cache.get(1)); // 返回 -1 (未找到)
console.log(cache.get(3)); // 返回 3
console.log(cache.get(4)); // 返回 4
总结
好了,到这里本篇的探究就结束了。在开头,我们先了解了KeepAlive
组件的基本使用方法,知道了KeepAlive
组件是一个抽象组件,只会渲染插槽的子vnode
,它自身并没有任何实际内容。我们从渲染 ,缓存 ,props 和卸载 四个维度去探究了KeepAlive
组件的源码实现,在渲染,卸载时由于shapeFlag
的不同processComponent
方法和unmount
方法都会去执行KeepAlive
专门的处理分支。最后,我们还简单了解了LRU算法的实现。