解读keep-alive:Vue3中手动清理keep-alive组件缓存的一个解决方案
作者:互联网
用过vue的同学肯定对keep-alive组件不陌生,它允许我们使用key对组件进行缓存,当使用相同key的组件渲染时,就会使用缓存中的组件,这样可以加快渲染速度,特别是在使用路由跳转时,效果是很明显的,而缓存就意味着更多的内存消耗,但是很遗憾,keep-alive组件不允许我们手动释放,我们唯一能操作keep-alive组件的的地方就是三个属性:
interface KeepAliveProps {
/**
* 如果指定,则只有与 `include` 名称
* 匹配的组件才会被缓存。
*/
include?: MatchPattern
/**
* 任何名称与 `exclude`
* 匹配的组件都不会被缓存。
*/
exclude?: MatchPattern
/**
* 最多可以缓存多少组件实例。
*/
max?: number | string
}
type MatchPattern = string | RegExp | (string | RegExp)[]
文档地址:https://cn.vuejs.org/api/built-in-components.html#keepalive
想要随心所欲的清理keep-alive组件的缓存,显然这三个属性时满足不了我们的要求的。
源码
我们先看看keep-alive相关的源码:
KeepAlive相关部分源码
const KeepAliveImpl = {
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, { slots }) {
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;
// 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 (!sharedContext.renderer) {
return () => {
const children = slots.default && slots.default();
return children && children.length === 1 ? children[0] : children;
};
}
const cache = new Map();
const keys = new Set();
let current = null;
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
instance.__v_cache = cache;
}
const parentSuspense = instance.suspense;
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 /* MoveType.ENTER */, parentSuspense);
// in case props have changed
patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, 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);
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance);
}
};
sharedContext.deactivate = (vnode) => {
const instance = vnode.component;
move(vnode, storageContainer, null, 1 /* 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 ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance);
}
};
function unmount(vnode) {
// reset the shapeFlag so it can be properly unmounted
resetShapeFlag(vnode);
_unmount(vnode, instance, parentSuspense, true);
}
function pruneCache(filter) {
cache.forEach((vnode, key) => {
const name = getComponentName(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) {
// 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]) => {
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
let pendingCacheKey = null;
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree));
}
};
onMounted(cacheSubtree);
onUpdated(cacheSubtree);
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance;
const vnode = getInnerChild(subTree);
if (cached.type === vnode.type) {
// 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;
}
const children = slots.default();
const rawVNode = 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(rawVNode) ||
(!(rawVNode.shapeFlag & 4 /* ShapeFlags.STATEFUL_COMPONENT */) &&
!(rawVNode.shapeFlag & 128 /* ShapeFlags.SUSPENSE */))) {
current = null;
return rawVNode;
}
let vnode = getInnerChild(rawVNode);
const comp = vnode.type;
// for async components, name check should be based in its loaded
// inner component if available
const name = getComponentName(isAsyncWrapper(vnode)
? vnode.type.__asyncResolved || {}
: comp);
const { include, exclude, max } = props;
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 & 128 /* 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.
pendingCacheKey = key;
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.shapeFlag |= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
// make this key the freshest
keys.delete(key);
keys.add(key);
}
else {
keys.add(key);
// prune oldest entry
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value);
}
}
// avoid vnode being unmounted
vnode.shapeFlag |= 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */;
current = vnode;
return isSuspense(rawVNode.type) ? rawVNode : vnode;
};
}
};
function resetShapeFlag(vnode) {
let shapeFlag = vnode.shapeFlag;
if (shapeFlag & 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */) {
shapeFlag -= 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */;
}
if (shapeFlag & 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */) {
shapeFlag -= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
}
vnode.shapeFlag = shapeFlag;
}
function getComponentName(Component, includeInferred = true) {
return isFunction(Component)
? Component.displayName || Component.name
: Component.name || (includeInferred && Component.__name);
}
function matches(pattern, name) {
if (isArray(pattern)) {
return pattern.some((p) => matches(p, name));
}
else if (isString(pattern)) {
return pattern.split(',').includes(name);
}
else if (pattern.test) {
return pattern.test(name);
}
/* istanbul ignore next */
return false;
}
查看源码可以发现,max、include、exclude三个属性的分别用在这几个地方:
max的作用在渲染函数的最后:
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.shapeFlag |= 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */;
// make this key the freshest
keys.delete(key);
keys.add(key);
}
else {
keys.add(key);
// prune oldest entry
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value);
}
}
可以看到,如果没有设置max,keep-alive组件默认只缓存10个实例,而keys是一个Set对象,它不停地进行delete和add,就是调整组件的访问顺序,也就是说keys中最开头的那个就是最久未被访问的,这样当缓存达到上限后,就可以直接弹出第一个key进行释放。注意,这里仅仅是释放一个缓存,也就是说,如果开始时max=10,当缓存组件达到10个后,然后程序中将max设置成5,这时keep-alive组件只会清理一个缓存,也就是说,还有9个缓存组件!如果要将缓存组件数降下来,我们只能先设置max=9,然后设置max=8。。。。
include、exclude属性作用在两个地方,一个是在渲染函数中,主要用于判断是否需要进行缓存:
let vnode = getInnerChild(rawVNode);
const comp = vnode.type;
// for async components, name check should be based in its loaded
// inner component if available
const name = getComponentName(isAsyncWrapper(vnode)
? vnode.type.__asyncResolved || {}
: comp);
const { include, exclude, max } = props;
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) {
current = vnode;
return rawVNode;
}
一个是在watch函数中,主要是监听include、exclude来调整缓存组件状态,需要注意的是,watch中flush设置为post:
function pruneCache(filter) {
cache.forEach((vnode, key) => {
const name = getComponentName(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) {
// 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]) => {
include && pruneCache(name => matches(include, name));
exclude && pruneCache(name => !matches(exclude, name));
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true });
从上面的源码可以看到,无论在渲染函数中还是watch函数中,都需要使用两个重要的函数:getComponentName和matches
function getComponentName(Component, includeInferred = true) {
return isFunction(Component)
? Component.displayName || Component.name
: Component.name || (includeInferred && Component.__name);
}
function matches(pattern, name) {
if (isArray(pattern)) {
return pattern.some((p) => matches(p, name));
}
else if (isString(pattern)) {
return pattern.split(',').includes(name);
}
else if (pattern.test) {
return pattern.test(name);
}
/* istanbul ignore next */
return false;
}
getComponentName函数中的Component参数其实就是选项式Api中的 this.$.type 对象,或者组合式Api中setup中的 getCurrentInstance().type 对象,而这个type其实是组件的类型对象,同一个组件的多个实例共享同一个type对象,getComponentName函数中优先取它的name属性,没有则取 __name 属性。
matches函数中的pattern参数可以是一个数组、逗号隔开的字符串、包含一个test函数的对象(不一定是ReExp对象,也可以是一个继承ReExp对象的自定义类型)。
手动释放的一个解决方案
从上面的源码可以看到,keep-alive组件的三个属性在能起到一些作用,如果想随心所欲的来清理keep-alive缓存,这三个属性就不够用了。
除此之外,在源码中可以看到,keep-alive的缓存保存在一个Map对象中:
const cache = new Map();
const keys = new Set();
let current = null;
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
instance.__v_cache = cache;
}
可以看到,这个缓存对象两种情况下会挂载到keep-alive组件中:
1、process.env.NODE_ENV !== 'production':表明它是作用在开发环境下
2、__VUE_PROD_DEVTOOLS__:表示是否启用了devtool
对于process.env.NODE_ENV变量:
如果是webpack4,可以在.env文件中设置NODE_ENV变量值进行覆盖。
如果是webpack5,貌似它的值就不能改动了,一般在开发环境中,它的值是development,build之后就是production
对于__VUE_PROD_DEVTOOLS__变量,我们只需要启用即可,添加vue.config.js,内容如下:
module.exports = {
configureWebpack: {
devtool: "source-map", //设置成false表示关闭
},
chainWebpack: (config) => {
config.plugin("define").tap((definitions) => {
for (const definition of definitions) {
if (definition) {
Object.assign(definition, {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: true,
});
}
}
return definitions;
});
}
};
将缓存Map对象挂载到keep-alive组件之后,我们就可以通过$refs取到keep-alive组件对象,进而得到缓存Map对象了。
为了方便使用,我们可以创建一个类来简化操作:
handler.js
export class KeepAliveHandler {
constructor() {
this._ = {};
}
get keys() {
const { cache } = this._
if (!cache || !cache()) {
return [];
}
return [...cache().keys()];
}
//绑定keepAlive信息
bind(keepAlive) {
if (keepAlive && keepAlive.$.__v_cache) {
const sharedContext = keepAlive.$.ctx;
const instance = keepAlive.$;
const { suspense: parentSuspense, __v_cache: cache } = instance;
const {
renderer: { um: unmount },
} = sharedContext;
Object.assign(this._, {
cache() {
return cache;
},
unmount(vnode) {
resetShapeFlag(vnode);
unmount(vnode, instance, parentSuspense, true);
},
isCurrent(key) {
return keepAlive.$.subTree && keepAlive.$.subTree.key === key
}
});
} else {
console.warn('当且仅当开发环境或者启用了devtool时生效')
}
}
//删除指定key的缓存
remove(key, reset = true) {
pruneCache.call(this, k => key !== k, reset)
}
//清空
clear() {
pruneCache.call(this, () => false, false)
}
}
function pruneCache(filter, reset) {
const { cache, unmount, isCurrent } = this._
if (!cache || !cache()) {
return
}
const c = cache()
c.set = new Map().set
c.forEach((vnode, key) => {
if (!filter(key)) {
if (isCurrent(key)) {
//重写set,因为渲染函数可能会重新执行
//这样就会导致缓存重新添加,导致清除失败
if (reset) {
c.set = function () {
c.set = new Map().set
}
}
resetShapeFlag(vnode)
} else {
unmount(vnode);
}
c.delete(key);
}
});
}
function resetShapeFlag(vnode) {
let shapeFlag = vnode.shapeFlag;
if (shapeFlag & 256) {
shapeFlag -= 256;
}
if (shapeFlag & 512) {
shapeFlag -= 512;
}
vnode.shapeFlag = shapeFlag;
}
一个简单的例子:
创建一个vue3的项目,包含vue-router,然后创建两个vue组件:
home.vue
<template>
<div class="home">
<h1>This is an home page</h1>
<h1>route:{{ $route.fullPath }}</h1>
<h1>time:{{ now }}</h1>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
now: new Date().toLocaleString(),
};
},
mounted() {
console.log("home mounted");
},
unmounted() {
console.log("home unmounted");
},
};
</script>
about.vue
<template>
<div class="about">
<h1>This is an about page</h1>
<h1>route:{{ $route.fullPath }}</h1>
<h1>time:{{ now }}</h1>
</div>
</template>
<script>
export default {
name: "About",
data() {
return {
now: new Date().toLocaleString(),
};
},
mounted() {
console.log("about mounted");
},
unmounted() {
console.log("about unmounted");
},
};
</script>
vue-router注册路由:
route.js
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue"),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
如果不使用keep-alive,App.vue的内容如下:
<template>
<div>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view></router-view>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
cursor: pointer;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
这时运行后,来回切换home和about页面,会发现两个页面每次都是重新渲染,其中的now属性一直在获取当前最新的时间,如果开发者工具中看控制台输出,会发现里面不停的打印:about mounted、about unmounted、home mounted、home unmounted。
如果使用keep-alive后,那么修改App.vue:
<template>
<div>
<div id="nav">
<router-link to="/">Home</router-link>
(<a href="#" @click="remove('/')">x</a>)|
<router-link to="/about">About</router-link>
(<a href="#" @click="remove('/about')">x</a>)
</div>
<router-view v-slot="{ Component }">
<keep-alive ref="keepAlive">
<component :is="Component" :key="$route.fullPath"></component>
</keep-alive>
</router-view>
</div>
</template>
<script>
import { KeepAliveHandler } from "@/handler";
import { onMounted, getCurrentInstance } from "vue";
export default {
setup() {
const instance = getCurrentInstance();
const handler = new KeepAliveHandler();
onMounted(() => {
const keepAlive = instance.refs.keepAlive;
handler.bind(keepAlive);
});
const remove = (key) => {
handler.remove(key);
};
return {
remove,
};
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
cursor: pointer;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
这里先创建一个KeepAliveHandler实例,然后在onMounted钩子中进行keep-alive组件的绑定,接下来就可以使用KeepAliveHandler实例的remove函数来删除指定key的缓存组件了。
项目运行后,来回切换home和about页面,会发现页面中的时间没有更新了,而且组件的mounted也只调用了一次,这就是keep-alive组件缓存机制在起作用。
接着,分别点击home和about旁边的 x ,再来回切换home和about页面,会发现页面中的事件更新了一次,而且组件对应的unmounted也执行了,这就说明成功清理了keep-alive的缓存,来回切换页面时重新渲染了页面并缓存
结语
其实keep-alive组件手动释放的问题由来已久,从原来vue2就开始有这个问题了,但是不知道为何vue的作者一直没有修复,github上也有不少吐槽这点的issue。
虽然这样子可以解决手动释放keep-alive缓存的问题,但是需要production环境启用devtools或者将process.env.NODE_ENV设置成非production,但是遗憾的事,在生产环境,我们恰恰是相反的,所以这种不是一个好的解决方案,只是利用vue给我们开的一扇窗子而已。
其次,经过测试,__VUE_PROD_DEVTOOLS__可以覆盖,但有时候又覆盖不了,貌似是版本的问题,我这边暂时使用的是webpack,有兴趣的可以使用vite试试,具体是可以参考github上给出的说明:https://github.com/vuejs/core/tree/main/packages/vue#bundler-build-feature-flags
后续有时间在仔细看看源码,看看有没有可以钻空子的地方。
标签:const,name,cache,alive,vnode,instance,key,Vue3,keep 来源: https://www.cnblogs.com/shanfeng1000/p/16692266.html