Add API to manually remove/unmount component in <keep-alive> cache
What problem does this feature solve?
When using vue-router to navigate in a larger SPA, keep-alive is a very nice way to keep views/components that are frequently visited in the cache. When the instance of the component are cached, one avoids time and energy to reload it into memory (especially GPU related assets, which otherwise would cause unnecessary loading delay for the user).
In other words, keep-alive helps building a snappy and smooth experience for the user.
However, this also means that the components that have been visited are adding up in the memory, and can also consume some CPU even if they are never to be visited again by the user.
One can argue that the "max" property can limit this, but, I think there can be many different reasons to control this manually instead. In our specific case, we would like to control this for performance reasons (based on frequency of times the component is visited, time since last visit, how time consuming is the component to load, memory consumption of the component, etc.).
Right now we have managed to fix this with a fork, where we expose the function pruneCacheEntry(key: CacheKey) in KeepAlive.ts, but it would have been much better if this was a part vue itself.
What does the proposed API look like?
<keep-alive ref="aliveKeeper">
</keep-alive>
...
this.$refs.aliveKeeper.removeFromCache(myVueComponent);
This has been discussed multiple times: https://github.com/vuejs/vue/issues/6259 https://github.com/vuejs/vue/issues/8028 https://github.com/vuejs/vue/issues/10487.
This is probably worth going through an RFC by comparing existing solutions with include, exclude and max
If include/exclude could match some unique feature of the instance (type or the instance itself), it might be sufficient, but I am not sure if that is a better solution than just explicitly remove an instance of a vue component from the cache.
Closed, tracking here https://github.com/vuejs/rfcs/pull/284
I used the following code, it seems ok:
<router-view v-slot="{Component, route}">
<keep-alive ref="keepAlive"><component :key="route.path" :is="Component" /></keep-alive>
</router-view>
onCloseTab(targetName) {
const route = this.tabRoutes.find(route => targetName === route.params.f_id)
this.$store.dispatch('removeTab', targetName)
// 删除组件在keepalive里的cache,参考 pruneCacheEntry 这个方法的逻辑
const key = route.path
const keepAliveInstance = this.$refs.keepAlive.$
const cache = keepAliveInstance.__v_cache
const vnode = cache.get(key)
if (vnode) {
let shapeFlag = vnode.shapeFlag
if (shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
shapeFlag -= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
}
if (shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
shapeFlag -= 512 /* COMPONENT_KEPT_ALIVE */
}
vnode.shapeFlag = shapeFlag
const renderer = keepAliveInstance.ctx.renderer
renderer.um(vnode, keepAliveInstance, keepAliveInstance.suspense)
cache.delete(key)
}
}
@yuanjinyong I did the similar trying before, but the __v_cache only exposed in develop mode, can not used in production.
Any update, hack or work around? I'm trying to achieve something like this example, Where you can dynamically add, remove or change tabs components and there is no way to clear the removed components cache.
Thanks, and by the way, Vue3 is just ✨ awesome ✨
I encountered the same issue when trying to implement tabs that share a same component. When a tab is closed by user, I would like to delete the cache of that particular instance only. Here is what I think would be the most elegant way to tackle this in Vue3.
Since KeepAlive takes include
/exclude
that accepts component names only, one of the most obvious solution to this problem is to wrap the shared component into an HOC that gives every instance a unique name
. Here is my implementation:
const createUniqueComponent = (component) => {
return markRaw(
defineComponent({
name: createUniqueId(), // any unique string generation method of your choice
components: {
uc: component
},
render() {
return h(this.$.components.uc, null, this.$slots)
}
})
)
}
Then, use it where you need to create the component instance, for example, in the router or a store. In my case, I am dynamically importing the component, so the code would look something like this:
tabs.push({
...
page: createUniqueComponent(defineAsyncComponent(() => import('@/views/SomeTabPage')))
})
Finally, generate a computed property for use as the include
prop for KeepAlive.
const keepAliveIncludes = computed(() => tabs.map((tab) => tab.page.name))
Now bind it to the include
prop of your KeepAlive and enjoy the magic:
<KeepAlive :include="keepAliveIncludes">
<component :is="tab.page">
</KeepAlive>
In the above example, when you remove an item from the tabs
array (user closes the tab), the cache of that particular instance will be removed too, whilst the remaining instances that use the same component will stay and keep their states. You can confirm this in your Vue DevTools.