前言
本文将从原理+应用+源码(Vue2和Vue3)的角度全面介绍 组件,全文共计16000字,阅读时间大概30min+,建议码住在看,相信看完本文的你会对该组件有一更深刻的认识。
一、<KeepAlive>是什么?
<KeepAlive>是一个特殊的抽象组件,它在Vue.js中用于缓存组件实例和状态,从而实现组件状态的保留以避免重复渲染。使用<KeepAlive>组件可以提高性能,减少不必要的组件创建,优化用户体验。 为什么说它是一个抽象组件呢?因为它并不会渲染自己的DOM元素,也不会出现在组件链中。它的主要作用是提供一个逻辑抽象,用于处理组件的缓存和生命周期。在该组件的内部,会使用一个缓存对象(Vue2为JS对象Vue3为Map对象)来存储被包裹的组件实例。当组件被切换到后台时,实例不会被销毁,而是被保存在缓存对象中;当组件再次被激活时,会直接从缓存对象中恢复实例,而不是重新创建。为了方便用户在使用该组件时进一步完成附加任务,其对应添加了两个生命周期钩子,这两个钩子允许我们在组件激活和停用时执行特定的逻辑。:
activated: 当组件被激活时,会触发组件的activated生命周期钩子;
deactivated: 当组件被切换到后台时,则会触发deactivated生命周期钩子。
除了上述两个生命周期钩子以外,该组件还提供了include、exclude以及max属性来进一步实现按照用户的需求,进行性能更好的缓存模式。
include: /指定哪些组件应该被缓存。/
exclude: /指明哪些组件不应该被缓存。
max : / 限制可被缓存的最大组件实例数。
include、exclude接受字符串、数组或正则表达式作为参数,用于匹配组件的名称。max则常常绑定数字类型限制最大缓存数(也兼容了字符串类型),贴段官网代码:
<!-- 以英文逗号分隔的字符串 -->
<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>
<!-- 限制最大缓存实例数为10-->
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
二、 <keep-alive>使用场景有哪些?
当页面符合以下几点时就可以利用该组件进行缓存,从而达到页面优化的效果。
- 路由切换:在单页面应用(SPA)中,在不同的路由页面之间切换时,如果不希望每次切换都重新加载和渲染页面,可以使用 <keep-alive>来缓存这些页面。实际开发中常常有以下场景,有一个列表页面并且列表的每一项都会对应跳转到相应的详情页面,当用户从列表页面点击一项进入详情页面,然后再次返回到列表页面时,往往希望列表页面能保持原来的滚动位置和状态,这就可以使用 <keep-alive>来缓存列表页面。
- 标签页切换:在一些复杂的界面中,可能会使用标签页(Tab)来切换不同的内容。如果每次切换标签页都重新渲染内容,可能会导致性能问题,尤其是当标签页的内容包含大量数据或复杂的组件时。在这种情况下,也可以使用 <keep-alive>来缓存标签页的内容。
- 条件渲染:在使用v-if或v-show进行条件渲染时,如果希望在条件变化时保留组件的状态,而不是每次都重新创建和销毁组件,也可以使用 <keep-alive>来缓存组件。
- 性能优化:在一些性能敏感的场景中,例如列表滚动、动画等,可通过缓存组件来避免不必要的渲染,提高性能。
- 数据持久化:在一些需要保持数据状态的场景中,例如表单填写、游戏等,可通过 <keep-alive>使用户离开页面后能保持原来的数据,以便用户返回时可以继续操作。
三、 <keep-alive>内部实现原理
3.1在VUE2中:
3.1.1缓存机制中的核心数据结构
- cache{}: <keep-alive>组件的内部缓存存储依赖于一个名为cache的对象,该对象用于存储被包裹的组件实例。每个组件实例在cache对象中的键是根据组件的名称或 tag 生成的,值是对应的组件实例。当需要渲染一个组件时, <keep-alive>会首先检查cache对象中是否已经存在对应的组件实例。如果存在,直接使用缓存中的实例进行页面渲染;如果不存在,创建一个新的实例,将其加入到cache对象中,然后进行页面渲染。
- keys[]:当缓存空间不足或者组件缓存数已经达到限制的max值时,会使用最近最少使用(LRU)淘汰策略进行内存清理,该策略核心思想是: /如果一个数据在最近一段时间内没有被访问到,那么在将来一段时间内它被访问的概率也很低。/ 因此,当缓存空间不足时,LRU策略会优先淘汰最近最少使用的数据。该缓存策略实现则依赖于该keys数组,这个数组用于存储cache对象中的键,数组的尾部是最近被访问的键,数组的头部则是最久未被访问的键。当像cache{}中添加一个缓存项时,也会像keys[ ]中push该缓存的key;当淘汰一个缓存项, <keep-alive>会删除keys数组头部的键,以及cache对象中对应的值。
3.1.2缓存的核心实现函数——render( )+cacheVNode( )
缓存机制的实现依赖两个核心函数一是 render()
「确认需要缓存的vNode及对应的组件实例」,二是 cacheVNode()
「存储刚才确认要缓存的实例」:
1、render ( ),这是一个Vue自带的函数,它并不属于组件的 methods,用于生成组件的虚拟 DOM(VNode),从而决定如何渲染组件。在 Vue 的内部实现中,组件实例会在创建过程中自动获取 render 函数,并在适当的时机调用它来生成虚拟 DOM。大多数情况下,不需要手动编写 render 函数,因为 Vue 会根据组件的模板(template)自动生成 render 函数。不过对于 <keep-alive> 组件, render( )起着至关重要的作用,它需要负责确定哪些组件实例需要被缓存以及从缓存中获取组件实例。
2、cacheVNode( ): 该函数声明于method方法中,用于将需要缓存的组件实例添加到缓存对象中。这个方法在组件的 mounted 和update生命周期钩子中被调用,也会在 include 或 exclude 属性发生变化时,通过watch 监听器进行调用。
在进行组件缓存时,两者执行顺序如下:
- 1️⃣首先执行render 函数,在这个过程中,如果发现有组件需要被缓存,会将该组件的 vNode 和 key 保存在组件实例的 vnodeToCache 和 keyToCache 属性中,从而方便接下来在cacheVNode中保存缓存实例于cache中,并将组件设置为已缓存状态。
- 2️⃣然后在 mounted 生命周期钩子中调用 cacheVNode 方法。cacheVNode 方法会检查 vnodeToCache(存储需要被缓存的组件实例VNode) 和 keyToCache(存储对应的键key) 属性,如果它们存在,则将对应的组件实例添加到缓存对象中。
⚠️⚠️⚠️ 注意, render()
不仅包含了确认需要缓存vNode逻辑,还包含再次进入缓存页面时直接返回已经缓存的组件逻辑。以一个实际的场景为例,你可以更清楚的了解这个过程:
一个 Vue 应用有两个页面,分别为首页(Home)和详情页(Detail),并且使用了 Vue Router 进行路由管理。为了优化性能,对首页使用 <keep-alive> 进行缓存。
-
首次访问首页:当用户首次访问首页时, <keep-alive> 组件的 render( )会被执行。因为这是首页组件的首次渲染,cache和keys都为空,所以 render( )将返回新创建的vNode(包含对应的组件实例)。然后,cacheVNode 函数会被调用,将首页组件实例添加到cache中,并记录其键添加到keys。这个过程中,render 函数和 cacheVNode 函数各被执行一次。
-
从首页跳转到详情页:当用户从首页跳转到详情页时,首页组件会被停用,但其状态和属性会被 <keep-alive>组件保留在cache中。这个过程中,render 函数和 cacheVNode 函数都不会被执行。
-
从详情页返回首页:当用户从详情页返回首页时, <keep-alive> 组件的 render 函数会再次被执行。这次,render 函数在cache中找到了首页组件实例,所以它将返回这个已经缓存的组件实例。然后,执行mounted生命周期钩子,由于在首次执行时vnodeToCache这个过程中cacheVNode 函数不会被执行。这是因为cacheVNode只有在mounted和update生命周期钩子中被调用,但此时 <keep-alive> 组件已经挂载过了,所以它的 mounted 生命周期钩子不会再次执行,并且没有涉及到update,所以cacheVNode 函数也就不会被调用了。
了解完这些前缀知识后,先来看一下 render( )是如何完成了上述的任务,以及其代码如何实现:
render( )
render()
函数首先获取默认插槽的第一个组件子节点(vNode),并检查其是否具有 componentOptions。如果 vNode存在且具有 componentOptions,则表示它是一个组件,需要进行缓存处理。- 函数检查组件名称是否匹配 include 和 exclude 规则。如果组件名称不在 include 中,或者在 exclude 中,则不会被缓存,函数直接返回 vNode。 如果组件需要被缓存,函数会根据 vNode.key 或componentOptions.Ctor.cid生成缓存键(key),并检查该键是否已经在缓存对象(cache)中。如果组件已经在缓存中,函数会复用缓存的组件实例,并将该组件的键(key)移动到keys 数组的末尾,表示最近访问。 如果组件不在缓存中,函数会将 vNode 和 key 保存在组件实例的 vnodeToCache和 keyToCache 属性中,以便在组件更新时进行缓存。
- 最后,函数设置 vnode.data.keepAlive 为 true,表示该组件实例被缓存。函数返回vNode,或者插槽中的第一个子节点(如果 vNode 不存在)。
render () {
// 获取默认插槽的第一个组件子节点
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
// 如果 vnode 存在且有 componentOptions(表示 vnode 是一个组件)
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// 不在 include 中,或者在 exclude 中的组件不会被缓存
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 满足缓存条件则处理组件缓存
const { cache, keys } = this
const key: ?string = 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)
} else {
// 延迟设置缓存,直到组件更新
this.vnodeToCache = vnode
this.keyToCache = key
}
// 设置 vnode 的 keepAlive 属性,表示该组件实例被缓存
vnode.data.keepAlive = true
}
// 返回 vnode,或者插槽中的第一个子节点(如果 vnode 不存在)
return vnode || (slot && slot[0])
}
}
通过这个 render 函数, <keep-alive> 组件可以在渲染时对被包裹的组件进行缓存处理,实现组件状态的保留和避免重复渲染。
cacheVNode( )
接下来再看一下 cacheVNode()
完成了哪些任务呢:
- 首先从获取 cache、keys、vnodeToCache 和 keyToCache。cache是用于存储缓存的组件实例的对象,keys是一个数组,用于记录组件实例在缓存中的顺序,cache和keys这两个值都是在created生命周期中进行初始化为空对象和空数组的。vnodeToCache是一个js对象,用来临时存储需要被缓存的组件实例(VNode),keyToCache为一个数组用来存储该组件实例对应的键。vnodeToCache和keyToCache则是在
render()
中被初始化赋值。上述的四个值在前文都有提到过。 - 检查 vnodeToCache 是否存在,如果存在,则表示有组件实例需要被缓存。此时,方法会从 vnodeToCache 中获取三个值:
tag:表示虚拟 DOM 节点(VNode)的标签名称,通常是一个以 vue-component- 开头的字符串,后面跟着组件的名称或 ID)
componentInstance :是一个 Vue 组件实例,表示当前 VNode 对应的组件对象,可以通过该属性访问和操作组件的属性、方法、数据等)
componentOptions: 是一个对象,包含了组件 VNode 的配置选项。如组件属性(props)、事件监听器(listeners)、插槽(slots)等。通过componentOptions可以获取组件的名称、构造函数等信息。
然后将组件实例添加到 cache 对象中。同时,将组件实例的键(keyToCache)添加到 keys 数组中,用于记录组件在缓存中的顺序。 - 如果设置了 max 属性,当缓存数量超过 max 时,方法会调用
pruneCacheEntry()
函数移除最早的缓存。 - 最后,方法将 this.vnodeToCache 设置为 null,表示当前没有组件实例需要被缓存。
通过 cacheVNode()
方法, <keep-alive> 组件可以将需要缓存的组件实例添加到缓存对象中,并根据 max 属性对缓存数量进行限制。
cacheVNode() {
// 从组件实例中获取 cache、keys、vnodeToCache 和 keyToCache
const { cache, keys, vnodeToCache, keyToCache } = this
// 检查 vnodeToCache 是否存在,如果存在,则表示有组件实例需要被缓存
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
// 将组件实例添加到 cache 对象中,同时存储组件名称、tag 和组件实例
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
// 将组件实例的 key 添加到 keys 数组中,用于记录组件在缓存中的顺序,并且遵循LRU策略将key存储于数组末端
keys.push(keyToCache)
// 如果设置了 max 属性,当缓存数量超过 max 时,移除最早的缓存
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
// 清空 vnodeToCache,确保同一个组件实例不会被多次添加到缓存中
this.vnodeToCache = null
}
}
3.1.3清理无需缓存Vnode——pruneCacheEntry+pruneCache
在淘汰缓存过程中 <keep-alive>实现了名为pruneCacheEntry和pruneCache的方法。
其中pruneCacheEntry 函数用于删除 /指定/ 的缓存项,在删除缓存项之前,它会调用组件实例的 destroy 方法,以便正确地销毁组件实例并触发相应的生命周期钩子,从而支持了缓存空间不足或者组件缓存数已经达到限制的max值时的淘汰策略。
而pruneCache则用于删除 /多个/ 缓存项,支持了include和exclude的实现。其原理为通过遍历cache对象,调用 pruneCacheEntry 函数。删除那些不在include属性中,或者在exclude属性中的缓存项。
下文是两个函数的详解:
1️⃣pruneCacheEntry( ) —删除指定的缓存项。首先,它会检查当前的虚拟节点(current)是否与缓存项的标签(entry.tag)相同,如果不同,则调用 entry.componentInstance.$destroy() 来销毁缓存的组件实例。然后,它会将缓存对象中对应的键(key)设置为 null,并从 keys 数组中移除该键。
function pruneCacheEntry (
cache: CacheEntryMap, // 缓存对象
key: string, // 要删除的缓存项的键
keys: Array<string>, // 缓存键的数组,用于记录缓存项的顺序
current?: VNode // 当前激活的虚拟节点(VNode),可选参数
) {
// 从缓存对象中获取要删除的缓存项
const entry: ?CacheEntry = cache[key]
// 如果缓存项存在,且当前激活的虚拟节点与缓存项的标签不同
if (entry && (!current || entry.tag !== current.tag)) {
// 销毁缓存的组件实例
entry.componentInstance.$destroy()
}
// 将缓存对象中对应的键设置为 null
cache[key] = null
// 从 keys 数组中移除该键
remove(keys, key)
}
2️⃣pruneCache ( )—用于遍历缓存对象,找出不满足 filter 条件的缓存项,然后删除它们。这个函数主要用于处理 <keep-alive> 组件的 include 和 exclude 属性的变化。
- 首先从 组件实例中获取 cache、keys 和 _vnode。cache是用于存储缓存的组件实例的对象,keys
是一个数组,用于记录组件实例在缓存中的顺序。_vnode 是组件的根虚拟节点。 - 函数接着遍历 cache 对象,对于每一个缓存项,函数会检查其名称是否符合 filter 函数的条件。filter
函数是一个回调函数,接受组件名称作为参数,返回一个布尔值,表示该组件是否应该被保留在缓存中。 - 如果缓存项的名称不符合 filter 函数的条件,函数会调用 pruneCacheEntry
函数来清理该缓存项。pruneCacheEntry 函数会从 cache 对象中删除指定的缓存项,并从 keys 数组中移除相应的键。 - 通过 pruneCache 函数, <keep-alive> 组件可以在 include 或 exclude
属性发生变化时,根据新的规则调整缓存,保证只有符合条件的组件实例被缓存。
function pruneCache (keepAliveInstance: any, filter: Function) {
// 从 \<keep-alive> 组件实例中获取 cache、keys 和 _vnode
const { cache, keys, _vnode } = keepAliveInstance
// 遍历 cache 对象
for (const key in cache) {
// 获取当前缓存项
const entry: ?CacheEntry = cache[key]
// 如果缓存项存在
if (entry) {
// 获取缓存项的名称
const name: ?string = entry.name
// 如果名称存在,且不符合 filter 函数的条件
if (name && !filter(name)) {
// 清理该缓存项
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
调用时机:pruneCache会在mounted周期中利用watch方法进行调用,以保证include和exclude属性的实现。pruneCacheEntry则会在cacheVNode方法中调用,以保证max属性的实现,以及在组件销毁时于$destoryed中清空所有缓存。
还有一些如props的声明、辅助函数的实现这里不再赘述,感兴趣的同学可以进一步阅读官网源码 :Vue2KeepAlive源码
3.2 在Vue3中:
Vue3在实现上结合了setup()
,不过其核心原理和Vue2并没有什么太大的差别,所以这里不再赘述缓存的核心原理,只针对一些不同点进行讲述。
3.2.1 区别1️⃣:缓存存储的数据结构类型
在Vue 3中, <keep-alive>组件改为使用名为**cache的 /Map对象/ 和名为keys的 /Set集合/ **来存储和管理缓存,这一改Vue2的存储数据结构。比起Vue2中的js对象和数组,Map与Set在插入、删除和查找操作上具有更高的性能,pruneCache()
和 pruneCacheEntry()
就应用了这个特性从而使缓存的清理更加高效。
3.2.2 区别2️⃣:缓存存储的核心函数逻辑
在前文对于Vue2的介绍中,我们清楚的知道缓存的核心逻辑依赖于两个函数一是render()
用来确定需要存储的vNode,二是 cacheVNode()
用来更新存储缓存节点信息相关的cache{}与key[]。Vue3在存储逻辑上与Vue2大差不差,但是在实现写法上还是有一定的区别的。
Vue3由于使用了setup语法糖,所以render函数对应的逻辑写成setup函数的return值即可(执行时机beforeMount 之后和 mounted 之前)。函数的主要任务是决定哪些子组件应该被缓存以及如何处理这些缓存,其实现逻辑如下:
首先,它会检查默认插槽中的子组件,确保只有一个子组件被包含,因为 \<keep-alive>
只能包含一个子组件。然后,根据子组件的类型(有状态组件、异步组件或 Suspense 组件)和 \<keep-alive>
组件的 include
和 exclude
属性,决定是否应该缓存子组件。
如果子组件应该被缓存,该函数会计算子组件的缓存键(key
),并尝试从缓存中获取对应的 VNode
。如果缓存中有对应的 VNode
,则会复用缓存的 VNode
,并将其标记为已保持活动状态,以避免它被当作新的实例挂载。同时,将缓存键移到 keys
集合的末尾,表示它是最新的缓存项。
如果缓存中没有对应的 VNode
,则会将新的 VNode
添加到缓存中,并将缓存键添加到 keys
集合。如果缓存的数量超过了 max
属性指定的值,它会删除最旧的缓存项。
() => {
// 重置 pendingCacheKey
pendingCacheKey = null
// 如果没有默认插槽,则返回 null
if (!slots.default) {
return null
}
// 获取子节点
const children = slots.default()
const rawVNode = children[0]
// 如果子节点数量大于 1,发出警告,并返回所有子节点
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
}
// 如果子节点不是一个有效的 VNode,或者子节点不是有状态组件且不是 Suspense 组件,则返回原始 VNode
else if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
return rawVNode
}
// 获取子节点的内部子节点
let vnode = getInnerChild(rawVNode)
const comp = vnode.type as ConcreteComponent
// 获取组件名称,如果是异步组件,则获取其已加载的内部组件名称(如果有)
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp,
)
// 获取 include、exclude 和 max 属性
const { include, exclude, max } = props
// 如果组件名称不在 include 中,或者在 exclude 中,则返回原始 VNode
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
current = vnode
return rawVNode
}
// 计算 vnode 的 key
const key = vnode.key == null ? comp : vnode.key
// 从缓存中获取 vnode
const cachedVNode = cache.get(key)
// 如果 vnode 被复用,克隆 vnode,因为我们将对其进行修改
if (vnode.el) {
vnode = cloneVNode(vnode)
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
// 将 pendingCacheKey 设置为 key,并在 beforeMount/beforeUpdate 钩子中缓存 instance.subTree(已规范化的 vnode)
pendingCacheKey = key
// 如果缓存中有 vnode
if (cachedVNode) {
// 复制挂载状态
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 如果 vnode 有过渡,递归更新子树上的过渡钩子
if (vnode.transition) {
setTransitionHooks(vnode, vnode.transition!)
}
// 避免 vnode 作为新实例挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// 将 key 设置为最新的
keys.delete(key)
keys.add(key)
} else {
// 将 key 添加到 keys 集合
keys.add(key)
// 如果达到最大缓存数量,删除最旧的缓存项
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// 避免 vnode 被卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
// 设置当前 vnode
current = vnode
// 如果是 Suspense 组件,则返回 rawVNode,否则返回 vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
(ps:这里补充说明一下60行处的代码“如果 vnode 被复用,克隆 vnode,因为我们将对其进行修改” 这个逻辑是因为我们需要对缓存的 VNode
进行一些修改,例如更新它的 el
和 component
属性以复用缓存的组件实例,以及更新 vnode.shapeFlag
以避免组件实例被当作新的实例挂载。这些修改可能会影响到 vnode
的状态和行为。为了确保修改不会影响到其他地方对原始 vnode
的引用,需要创建一个新的 vnode
副本,然后对这个副本进行修改。)
** cacheSubtree( )**
除了上述render函数的实现,Vue3还封装了一个函数cacheSubtree()
,其功能大概对标Vue2中cacheVNode( ),二者都用于将组件实例的子树(或 VNode)添加到 \<keep-alive>
组件的缓存中,并且都在mounted或update生命周期中执行。但是,它们在具体实现上有一些差异。通过前文我们知道setup的return函数中(render( ))有着对标于vue2的render( )函数的逻辑—决定是否缓存组件实例以及如何处理缓存,并且赋值了当前cache对应的key值,所以这里无需在考虑维护keys集合,只需要将节点加入到cache这个map对象中即可。
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
3.2.3 区别3️⃣:清理缓存存储的函数实现
pruneCache
和 pruneCacheEntry
的基本实现原理与 Vue 2 类似,都是用于删除缓存中的组件实例。但是,由于 Vue 3 的内部实现有所改变,所以这两个函数的具体实现也有一些不同。pruneCache
依旧是遍历cache,筛选缓存对象,与Vue2原理一致;但 pruneCacheEntry
在实现上相差较大。在Vue3中,这个函数会从缓存对象cache和 keys集合中删除指定的缓存项。在删除缓存项之前,pruneCacheEntry
函数会调用 unmount
函数,以便正确地卸载组件实例并触发相应的生命周期钩子。如果当前激活的组件实例与要删除的缓存项是同一个,pruneCacheEntry
函数会调用 resetShapeFlag
函数重置其标志,因为这个组件实例不再被 <keep-alive>组件保持活动状态。
总结一下,与 Vue 2 相比,Vue 3 中 pruneCache 和 pruneCacheEntry 函数的主要区别在于:
- Vue 3 使用 Map 对象而不是普通的 JavaScript 对象来存储缓存。Map 对象保证了缓存项的插入顺序,所以不再需要 keys 数组来保存缓存顺序。
- 在 Vue 3 中,pruneCacheEntry 函数会调用 unmount 函数卸载组件实例,而不是直接调用组件实例的 $destroy 方法。这是因为 Vue 3 的组件实例没有 destroy 方法,组件的卸载过程由 unmount 函数管理。
- 如果当前激活的组件实例与要删除的缓存项是同一个,Vue 3 会重置其标志,因为这个组件实例不应再被` <keep-alive>组件保持活动状态。这是 Vue 3 的新特性,Vue 2 没有这个处理。
Vue3中这两个函数的具体实现:
// pruneCache 函数用于根据过滤条件删除缓存中的组件实例
function pruneCache(filter?: (name: string) => boolean) {
// 使用 cache.forEach 方法遍历 Map 对象中的缓存项
cache.forEach((vnode, key) => {
// 获取组件实例的名称
const name = getComponentName(vnode.type as ConcreteComponent);
// 如果组件名称存在,且不满足过滤条件(filter 函数返回 false 或不存在)
if (name && (!filter || !filter(name))) {
// 调用 pruneCacheEntry 函数删除指定的缓存项
pruneCacheEntry(key);
}
});
}
// pruneCacheEntry 函数用于删除指定的缓存项
function pruneCacheEntry(key: CacheKey) {
// 从 cache 对象中获取指定的缓存项
const cached = cache.get(key) as VNode;
// 如果当前激活的组件实例与要删除的缓存项不是同一个
if (!current || !isSameVNodeType(cached, current)) {
// 调用 unmount 函数卸载组件实例
unmount(cached);
} else if (current) {
// 如果当前激活的组件实例与要删除的缓存项是同一个
// 重置组件实例的标志,表示该实例不再被 \<keep-alive> 组件保持活动状态
resetShapeFlag(current);
}
// 从 cache 对象中删除指定的缓存项
cache.delete(key);
// 从 keys 集合中删除指定的 key
keys.delete(key);
}
3.2.4 兼容了渲染模式判断
Vue3还兼容了对渲染模式的判断:如果是ssr服务器端渲染,则不需要处理组件实例的缓存,因为服务器端渲染只负责生成静态 HTML,而不涉及组件的动态交互。因此,在这种情况下, <keep-alive> 组件只需要渲染其子组件即可。具体判断逻辑如下:
- 首先,代码获取当前组件实例的上下文对象 sharedContext,sharedContext.renderer是 Vue 3 中的一个内部渲染器对象,它包含了一系列用于创建和更新虚拟 DOM 的方法。
- 然后,检查是否处于 SSR环境(SSR),并且内部渲染器(sharedContext.renderer)未注册。如果满足这两个条件,说明当前是在服务器端渲染环境下。
- 若在 SSR 环境下,setup 函数返回一个渲染函数,该函数直接返回 <keep-alive>
组件的子组件(slots.default())。如果子组件数组只包含一个元素,返回该元素;否则返回整个子组件数组。
const sharedContext = instance.ctx as KeepAliveContext
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}
四、总结
本文至此就已经结束了,相信对于 <keep-alive>
你已经有了更深刻的理解。Vue3的实现其实在代码层面(445行代码)比起Vue2(152行代码)添加了更多细节的判断,从代码数量看也可以直观的感受到vue3的升级,如对生命周期更细致化的定义,以及一些标识符来进一步提升性能…不过二者的核心还是相差不多的,我们只需要深刻的理解好Cache及Key两种数据结构的作用,更新缓存时的核心逻辑,以及缓存的清除策略就已经算是keep-alive组件的使用高手了。当然,如果你学有余力!也可以进一步剖析源码!