From aa0c13f637df7eb27faa2545ee731f543c0813ec Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Dec 2023 23:50:28 +0800 Subject: [PATCH] fix(Suspense): handle switching away from kept-alive component before resolve close #6416 using test from #6467 --- .../__tests__/components/Suspense.spec.ts | 52 ++++++++++++++++++- .../runtime-core/src/components/Suspense.ts | 13 +++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index d647a96ec..60dcedc97 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -19,7 +19,8 @@ import { shallowRef, SuspenseProps, resolveDynamicComponent, - Fragment + Fragment, + KeepAlive } from '@vue/runtime-test' import { createApp, defineComponent } from 'vue' import { type RawSlots } from 'packages/runtime-core/src/componentSlots' @@ -1638,6 +1639,55 @@ describe('Suspense', () => { expect(serializeInner(root)).toBe(expected) }) + // #6416 + test('KeepAlive with Suspense', async () => { + const Async = defineAsyncComponent({ + render() { + return h('div', 'async') + } + }) + const Sync = { + render() { + return h('div', 'sync') + } + } + const components = [Async, Sync] + const viewRef = ref(0) + const root = nodeOps.createElement('div') + const App = { + render() { + return h(KeepAlive, null, { + default: () => { + return h(Suspense, null, { + default: h(components[viewRef.value]), + fallback: h('div', 'Loading-dynamic-components') + }) + } + }) + } + } + render(h(App), root) + expect(serializeInner(root)).toBe(`
Loading-dynamic-components
`) + + viewRef.value = 1 + await nextTick() + expect(serializeInner(root)).toBe(`
sync
`) + + viewRef.value = 0 + await nextTick() + + expect(serializeInner(root)).toBe('') + + await Promise.all(deps) + await nextTick() + // when async resolve,it should be
async
+ expect(serializeInner(root)).toBe('
async
') + + viewRef.value = 1 + await nextTick() //TypeError: Cannot read properties of null (reading 'parentNode'),This has been fixed + expect(serializeInner(root)).toBe(`
sync
`) + }) + describe('warnings', () => { // base function to check if a combination of slots warns or not function baseCheckWarn( diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 469dd87cb..366f4a7bb 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -47,6 +47,9 @@ export interface SuspenseProps { export const isSuspense = (type: any): boolean => type.__isSuspense +// incrementing unique id for every pending branch +let suspenseId = 0 + // Suspense exposes a component-like API, and is treated like a component // in the compiler, but internally it's a special built-in type that hooks // directly into the renderer. @@ -249,7 +252,8 @@ function patchSuspense( } } else { // toggled before pending tree is resolved - suspense.pendingId++ + // increment pending ID. this is used to invalidate async callbacks + suspense.pendingId = suspenseId++ if (isHydrating) { // if toggled before hydration is finished, the current DOM tree is // no longer valid. set it as the active branch so it will be unmounted @@ -259,7 +263,6 @@ function patchSuspense( } else { unmount(pendingBranch, parentComponent, suspense) } - // increment pending ID. this is used to invalidate async callbacks // reset suspense state suspense.deps = 0 // discard effects from pending branch @@ -350,7 +353,11 @@ function patchSuspense( triggerEvent(n2, 'onPending') // mount pending branch in off-dom container suspense.pendingBranch = newBranch - suspense.pendingId++ + if (newBranch.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { + suspense.pendingId = newBranch.component!.suspenseId! + } else { + suspense.pendingId = suspenseId++ + } patch( null, newBranch,