diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 3a3d85ee..f75bb039 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -14,7 +14,8 @@ import { ComponentPublicInstance, Ref, cloneVNode, - provide + provide, + withScopeId } from '@vue/runtime-test' import { KeepAliveProps } from '../../src/components/KeepAlive' @@ -655,4 +656,30 @@ describe('KeepAlive', () => { expect(spyMounted).toHaveBeenCalledTimes(3) expect(spyUnmounted).toHaveBeenCalledTimes(4) }) + + // #1513 + test('should work with cloned root due to scopeId / fallthrough attrs', async () => { + const viewRef = ref('one') + const instanceRef = ref(null) + const withId = withScopeId('foo') + const App = { + __scopeId: 'foo', + render: withId(() => { + return h(KeepAlive, null, { + default: () => h(views[viewRef.value], { ref: instanceRef }) + }) + }) + } + render(h(App), root) + expect(serializeInner(root)).toBe(`
one
`) + instanceRef.value.msg = 'changed' + await nextTick() + expect(serializeInner(root)).toBe(`
changed
`) + viewRef.value = 'two' + await nextTick() + expect(serializeInner(root)).toBe(`
two
`) + viewRef.value = 'one' + await nextTick() + expect(serializeInner(root)).toBe(`
changed
`) + }) }) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 4cc7747b..1ef589d7 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -9,7 +9,13 @@ import { } from '../component' import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode' import { warn } from '../warning' -import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle' +import { + onBeforeUnmount, + injectHook, + onUnmounted, + onBeforeMount, + onBeforeUpdate +} from '../apiLifecycle' import { isString, isArray, @@ -173,6 +179,16 @@ const KeepAliveImpl = { } ) + // cache sub tree in beforeMount/Update (i.e. right after the render) + let pendingCacheKey: CacheKey | null = null + const cacheSubtree = () => { + if (pendingCacheKey) { + cache.set(pendingCacheKey, instance.subTree) + } + } + onBeforeMount(cacheSubtree) + onBeforeUpdate(cacheSubtree) + onBeforeUnmount(() => { cache.forEach(cached => { const { subTree, suspense } = instance @@ -189,6 +205,8 @@ const KeepAliveImpl = { }) return () => { + pendingCacheKey = null + if (!slots.default) { return null } @@ -227,7 +245,12 @@ const KeepAliveImpl = { if (vnode.el) { vnode = cloneVNode(vnode) } - cache.set(key, 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