#15710·Vue.js

在渲染返回已安装的 vnode 时,KeepAlive with max 会禁用而不是卸载已修剪的当前组件(实例泄漏)

作者: AibanHoriuchi创建于 2026年9月29日更新于 2026年9月29日

Steps to reproduce

  1. 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 A to B (a new cache key), so max: 1 prunes A.
  2. 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:

  • isUnmounted stays false, 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.afterEach registrations, and query observers keep running. onBeforeUnmount / onUnmounted / onScopeDispose never 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 calls router.replace and hijacks later navigations.

System Info

bash
Any browser / happy-dom. Reproduced with vue 3.5.43 (Nuxt 4.5.2 in the original app).

Any additional comments?

Cause.

  1. The render returns an already-mounted vnode (vnode.el is set). KeepAlive clones it and stores the clone as current.
  2. KeepAlive then returns that clone, which still has el. normalizeVNode (cloneIfMounted) in renderComponentRoot clones it once more, and it is this second clone that gets mounted (and cached by cacheSubtree).
  3. When pruneCacheEntry later prunes the entry of the currently displayed component, isSameVNodeType(cached, current) is true. So it only calls resetShapeFlag(current), which clears the keep-alive flag on a vnode that is not in the tree.
  4. 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 `...