在渲染返回已安装的 vnode 时,KeepAlive with max 会禁用而不是卸载已修剪的当前组件(实例泄漏)
作者: AibanHoriuchi创建于 2026年9月29日更新于 2026年9月29日
Steps to reproduce
- Click "Run".
- The parent first re-renders and returns the
<KeepAlive :max="1">vnode it rendered last time (already mounted), the way Nuxt's<NuxtPage>does while a parent route is kept alive. - It then switches the child from
AtoB(a new cache key), somax: 1prunesA.
- The parent first re-renders and returns the
- Look at the log.
What is expected?
A is unmounted: the log shows A unmounted.
What is actually happening?
The log only shows A deactivated. A is never unmounted:
isUnmountedstaysfalse, and its effect scope stays active.- It is moved into KeepAlive's storage container, but it is no longer in the cache, so nothing will ever unmount or reactivate it.
- Its watchers,
router.afterEachregistrations, and query observers keep running.onBeforeUnmount/onUnmounted/onScopeDisposenever run. In our Nuxt app, every time the user leaves a kept-alive route and comes back to a different child, one more such instance piles up. Their watchers then misbehave, e.g. a leaked page's watcher callsrouter.replaceand hijacks later navigations.
System Info
Any browser / happy-dom. Reproduced with vue 3.5.43 (Nuxt 4.5.2 in the original app).Any additional comments?
Cause.
- The render returns an already-mounted vnode (
vnode.elis set). KeepAlive clones it and stores the clone ascurrent. - KeepAlive then returns that clone, which still has
el.normalizeVNode(cloneIfMounted) inrenderComponentRootclones it once more, and it is this second clone that gets mounted (and cached bycacheSubtree). - When
pruneCacheEntrylater prunes the entry of the currently displayed component,isSameVNodeType(cached, current)is true. So it only callsresetShapeFlag(current), which clears the keep-alive flag on a vnode that is not in the tree. - The mounted vnode keeps
COMPONENT_SHOULD_KEEP_ALIVE, so the following patch deactivates it instead of unmounting it. The same happens when the child is wrapped in `...
内容来源: vuejs/core