From 398ecc9ed9e05f05f2902c3090702a7627a4e65e Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 24 Sep 2022 13:19:20 +0800 Subject: [PATCH 1/4] test: add failing test --- .../__tests__/components/Suspense.spec.ts | 143 +++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index a2c38b2846e..ee3b14a362e 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -12,7 +12,8 @@ import { watchEffect, onUnmounted, onErrorCaptured, - shallowRef + shallowRef, + Fragment } from '@vue/runtime-test' import { createApp } from 'vue' @@ -1253,4 +1254,144 @@ describe('Suspense', () => { `A component with async setup() must be nested in a ` ).toHaveBeenWarned() }) + + test('nested async components with dynamics', async () => { + const calls: string[] = [] + let expected = '' + + const InnerA = defineAsyncComponent( + { + setup: () => { + calls.push('innerA created') + onMounted(() => { + calls.push('innerA mounted') + }) + return () => h('div', 'innerA') + } + }, + 10 + ) + + const InnerB = defineAsyncComponent( + { + setup: () => { + calls.push('innerB created') + onMounted(() => { + calls.push('innerB mounted') + }) + return () => h('div', 'innerB') + } + }, + 10 + ) + + const OuterA = defineAsyncComponent( + { + setup: (_, { slots }: any) => { + calls.push('outerA created') + onMounted(() => { + calls.push('outerA mounted') + }) + return () => + h(Fragment, null, [h('div', 'outerA'), slots.default?.()]) + } + }, + 5 + ) + + const OuterB = defineAsyncComponent( + { + setup: (_, { slots }: any) => { + calls.push('outerB created') + onMounted(() => { + calls.push('outerB mounted') + }) + return () => + h(Fragment, null, [h('div', 'outerB'), slots.default?.()]) + } + }, + 5 + ) + + const outerToggle = ref(false) + const innerToggle = ref(false) + + /** + * + * + *
+ * + *
+ *
+ *
+ */ + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: [ + h(outerToggle.value ? OuterB : OuterA, null, { + default: () => h(innerToggle.value ? InnerB : InnerA) + }) + ], + fallback: h('div', 'fallback outer') + }) + } + } + + expected = `
fallback outer
` + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(expected) + + // mount outer component + await Promise.all(deps) + await nextTick() + + expect(serializeInner(root)).toBe(expected) + expect(calls).toEqual([`outerA created`]) + + // mount inner component + await Promise.all(deps) + await nextTick() + expected = `
outerA
innerA
` + expect(serializeInner(root)).toBe(expected) + + expect(calls).toEqual([ + 'outerA created', + 'innerA created', + 'outerA mounted', + 'innerA mounted' + ]) + + // toggle outer component + calls.length = 0 + deps.length = 0 + outerToggle.value = true + await nextTick() + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(expected) // expect not change + + await Promise.all(deps) + await nextTick() + expected = `
outerB
innerA
` + expect(serializeInner(root)).toBe(expected) + expect(calls).toContain('outerB mounted') + expect(calls).toContain('innerA mounted') + + // toggle inner component + calls.length = 0 + deps.length = 0 + innerToggle.value = true + await nextTick() + expect(serializeInner(root)).toBe(expected) // expect not change + + await Promise.all(deps) + await nextTick() + expected = `
outerB
innerB
` + expect(serializeInner(root)).toBe(expected) + expect(calls).toContain('innerB mounted') + }) }) From 1e4b2dcea323b28ef56956ca0986d29bedd63f75 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 24 Sep 2022 14:50:52 +0800 Subject: [PATCH 2/4] feat: introduce `suspensible` option to `` --- .../__tests__/components/Suspense.spec.ts | 10 +++--- .../runtime-core/src/components/Suspense.ts | 33 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index ee3b14a362e..710f96959d0 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -1255,7 +1255,7 @@ describe('Suspense', () => { ).toHaveBeenWarned() }) - test('nested async components with dynamics', async () => { + test('nested suspense with suspensible', async () => { const calls: string[] = [] let expected = '' @@ -1319,9 +1319,9 @@ describe('Suspense', () => { /** * * - *
+ * * - *
+ *
* *
*/ @@ -1331,7 +1331,9 @@ describe('Suspense', () => { h(Suspense, null, { default: [ h(outerToggle.value ? OuterB : OuterA, null, { - default: () => h(innerToggle.value ? InnerB : InnerA) + default: () => h(Suspense, { suspensible: true },{ + default: h(innerToggle.value ? InnerB : InnerA) + }) }) ], fallback: h('div', 'fallback outer') diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 8408cab388d..64676d36cce 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -30,6 +30,12 @@ export interface SuspenseProps { onPending?: () => void onFallback?: () => void timeout?: string | number + /** + * Allow suspense to be captured by parent suspense + * + * @default false + */ + suspensible?: boolean } export const isSuspense = (type: any): boolean => type.__isSuspense @@ -388,7 +394,7 @@ let hasWarned = false function createSuspenseBoundary( vnode: VNode, - parent: SuspenseBoundary | null, + parentSuspense: SuspenseBoundary | null, parentComponent: ComponentInternalInstance | null, container: RendererElement, hiddenContainer: RendererElement, @@ -416,10 +422,19 @@ function createSuspenseBoundary( o: { parentNode, remove } } = rendererInternals + // if set `suspensible: true`, set the current suspense as a dep of parent suspense + let parentSuspenseId: number | undefined + if (vnode.props?.suspensible) { + if (parentSuspense?.pendingBranch) { + parentSuspenseId = parentSuspense?.pendingId + parentSuspense.deps++ + } + } + const timeout = toNumber(vnode.props && vnode.props.timeout) const suspense: SuspenseBoundary = { vnode, - parent, + parent: parentSuspense, parentComponent, isSVG, container, @@ -511,6 +526,20 @@ function createSuspenseBoundary( } suspense.effects = [] + // resolve parent suspense if all async deps are resolved + if (vnode.props?.suspensible) { + if ( + parentSuspense && + parentSuspense.pendingBranch && + parentSuspenseId === parentSuspense.pendingId + ) { + parentSuspense.deps-- + if (parentSuspense?.deps === 0) { + parentSuspense.resolve() + } + } + } + // invoke @resolve event triggerEvent(vnode, 'onResolve') }, From 15a89c51c518e85d99570458540d991946d516f9 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 30 Sep 2022 17:23:25 +0800 Subject: [PATCH 3/4] refactor: support non-value prop --- packages/runtime-core/src/components/Suspense.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 64676d36cce..277d24ae8a6 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -424,7 +424,9 @@ function createSuspenseBoundary( // if set `suspensible: true`, set the current suspense as a dep of parent suspense let parentSuspenseId: number | undefined - if (vnode.props?.suspensible) { + let isSuspensible = + vnode.props?.suspensible != null && vnode.props.suspensible !== false + if (isSuspensible) { if (parentSuspense?.pendingBranch) { parentSuspenseId = parentSuspense?.pendingId parentSuspense.deps++ @@ -527,14 +529,14 @@ function createSuspenseBoundary( suspense.effects = [] // resolve parent suspense if all async deps are resolved - if (vnode.props?.suspensible) { + if (isSuspensible) { if ( parentSuspense && parentSuspense.pendingBranch && parentSuspenseId === parentSuspense.pendingId ) { parentSuspense.deps-- - if (parentSuspense?.deps === 0) { + if (parentSuspense.deps === 0) { parentSuspense.resolve() } } From 7869bcf820072de6c9e8586ddf82b87dddd1621d Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 2 Oct 2022 03:35:10 +0800 Subject: [PATCH 4/4] Update packages/runtime-core/src/components/Suspense.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Damian GÅ‚owala <48835293+DamianGlowala@users.noreply.github.com> --- packages/runtime-core/src/components/Suspense.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 277d24ae8a6..322516ee9cf 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -424,7 +424,7 @@ function createSuspenseBoundary( // if set `suspensible: true`, set the current suspense as a dep of parent suspense let parentSuspenseId: number | undefined - let isSuspensible = + const isSuspensible = vnode.props?.suspensible != null && vnode.props.suspensible !== false if (isSuspensible) { if (parentSuspense?.pendingBranch) {