From a67023f4a2271425bc68bc4d54e30f155596edb3 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 4 Nov 2025 21:42:38 +0800 Subject: [PATCH 1/4] feat(runtime-vapor): vapor transition work with vapor async component --- .../src/apiDefineAsyncComponent.ts | 15 +++++++++++---- packages/runtime-vapor/src/block.ts | 18 +++++++++++++----- .../runtime-vapor/src/components/Transition.ts | 16 +++++++++++----- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index dd6143950e3..4cde9454f09 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -26,8 +26,9 @@ import { removeFragmentNodes, } from './dom/hydration' import { invokeArrayFns } from '@vue/shared' -import { insert, remove } from './block' +import { type TransitionOptions, insert, remove } from './block' import { parentNode } from './dom/node' +import { setTransitionHooks } from './components/Transition' /*@ __NO_SIDE_EFFECTS__ */ export function defineVaporAsyncComponent( @@ -109,7 +110,8 @@ export function defineVaporAsyncComponent( }, setup() { - const instance = currentInstance as VaporComponentInstance + const instance = currentInstance as VaporComponentInstance & + TransitionOptions markAsyncBoundary(instance) const frag = @@ -166,6 +168,8 @@ export function defineVaporAsyncComponent( } else if (loadingComponent && !delayed.value) { render = () => createComponent(loadingComponent) } + + if (instance.$transition) frag!.$transition = instance.$transition frag!.update(render) }) @@ -176,10 +180,10 @@ export function defineVaporAsyncComponent( function createInnerComp( comp: VaporComponent, - parent: VaporComponentInstance, + parent: VaporComponentInstance & TransitionOptions, frag?: DynamicFragment, ): VaporComponentInstance { - const { rawProps, rawSlots, isSingleRoot, appContext } = parent + const { rawProps, rawSlots, isSingleRoot, appContext, $transition } = parent const instance = createComponent( comp, rawProps, @@ -189,6 +193,9 @@ function createInnerComp( appContext, ) + // set transition hooks + if ($transition) setTransitionHooks(instance, $transition) + // set ref // @ts-expect-error frag && frag.setRef && frag.setRef(instance) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 729ebee16fc..0f6d32ae8a5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -36,12 +36,20 @@ export interface TransitionOptions { $transition?: VaporTransitionHooks } -export type TransitionBlock = - | (Node & TransitionOptions) - | (VaporFragment & TransitionOptions) - | (DynamicFragment & TransitionOptions) +export type TransitionBlock = ( + | Node + | VaporFragment + | DynamicFragment + | VaporComponentInstance +) & + TransitionOptions -export type Block = TransitionBlock | VaporComponentInstance | Block[] +export type Block = + | Node + | VaporFragment + | DynamicFragment + | VaporComponentInstance + | Block[] export type BlockFn = (...args: any[]) => Block export function isBlock(val: NonNullable): val is Block { diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index a1f420deae5..9388040d0f6 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -10,6 +10,7 @@ import { baseResolveTransitionHooks, checkTransitionMode, currentInstance, + isAsyncWrapper, isTemplateNode, leaveCbKey, queuePostFlushCb, @@ -225,7 +226,7 @@ export function applyTransitionHooks( hooks => (resolvedHooks = hooks as VaporTransitionHooks), ) resolvedHooks.delayedLeave = delayedLeave - setTransitionHooks(child, resolvedHooks) + child.$transition = resolvedHooks if (isFrag) setTransitionHooksOnFragment(block, resolvedHooks) // fallthrough attrs @@ -253,7 +254,7 @@ export function applyTransitionLeaveHooks( state, instance, ) - setTransitionHooks(leavingBlock, leavingHooks) + leavingBlock.$transition = leavingHooks const { mode } = props if (mode === 'out-in') { @@ -302,7 +303,12 @@ export function findTransitionBlock( // transition can only be applied on Element child if (block instanceof Element) child = block } else if (isVaporComponent(block)) { - child = findTransitionBlock(block.block) + // should save hooks on unresolved async wrapper, so that it can be applied after resolved + if (isAsyncWrapper(block) && !block.type.__asyncResolved) { + child = block + } else { + child = findTransitionBlock(block.block) + } // use component id as key if (child && child.$key === undefined) child.$key = block.uid } else if (isArray(block)) { @@ -344,7 +350,7 @@ export function setTransitionHooksOnFragment( hooks: VaporTransitionHooks, ): void { if (isFragment(block)) { - setTransitionHooks(block, hooks) + block.$transition = hooks } else if (isArray(block)) { for (let i = 0; i < block.length; i++) { setTransitionHooksOnFragment(block[i], hooks) @@ -353,7 +359,7 @@ export function setTransitionHooksOnFragment( } export function setTransitionHooks( - block: TransitionBlock | VaporComponentInstance, + block: TransitionBlock, hooks: VaporTransitionHooks, ): void { if (isVaporComponent(block)) { From 94738daa2814cc326d8f9f80ee257a44853398c0 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Nov 2025 09:35:12 +0800 Subject: [PATCH 2/4] chore: tweaks --- .../runtime-vapor/src/components/Transition.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 9388040d0f6..2e9a035267e 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -90,7 +90,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate( if (child) { // replace existing transition hooks child.$transition!.props = resolvedProps - applyTransitionHooks(child, child.$transition!) + applyTransitionHooks(child, child.$transition!, undefined, true) } } } else { @@ -139,7 +139,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate( ) const getTransitionHooksContext = ( - key: String, + key: string, props: TransitionProps, state: TransitionState, instance: GenericComponentInstance, @@ -208,9 +208,12 @@ export function applyTransitionHooks( block: Block, hooks: VaporTransitionHooks, fallthroughAttrs: boolean = true, + isResolved: boolean = false, ): VaporTransitionHooks { const isFrag = isFragment(block) - const child = findTransitionBlock(block) + const child = isResolved + ? (block as TransitionBlock) + : findTransitionBlock(block) if (!child) { // set transition hooks on fragment for reusing during it's updating if (isFrag) setTransitionHooksOnFragment(block, hooks) @@ -288,15 +291,10 @@ export function applyTransitionLeaveHooks( } } -const transitionBlockCache = new WeakMap() export function findTransitionBlock( block: Block, inFragment: boolean = false, ): TransitionBlock | undefined { - if (transitionBlockCache.has(block)) { - return transitionBlockCache.get(block) - } - let isFrag = false let child: TransitionBlock | undefined if (block instanceof Node) { From 6916515b42bceacb7568190afc92ce52dfcd39bf Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Nov 2025 09:56:27 +0800 Subject: [PATCH 3/4] test: add tests --- .../__tests__/transition.spec.ts | 60 ++++++++++++++++++- .../vapor-e2e-test/transition/App.vue | 16 +++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index 0bfc30598cc..9be8a68fd83 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -13,7 +13,6 @@ const { nextFrame, timeout, isVisible, - count, html, transitionStart, waitForElement, @@ -907,6 +906,65 @@ describe('vapor transition', () => { describe.todo('transition with Suspense', () => {}) describe.todo('transition with Teleport', () => {}) + describe('transition with AsyncComponent', () => { + test('apply transition to inner component', async () => { + const btnSelector = '.async > button' + const containerSelector = '.async > div' + + expect(await html(containerSelector)).toBe('') + + // toggle + await click(btnSelector) + await nextTick() + // not yet resolved + expect(await html(containerSelector)).toBe('') + + // wait resolving + await timeout(50) + + // enter (resolved) + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await nextFrame() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + + // leave + await click(btnSelector) + await nextTick() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await nextFrame() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await transitionFinish() + expect(await html(containerSelector)).toBe('') + + // enter again + await click(btnSelector) + // use the already resolved component + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await nextFrame() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + await transitionFinish() + expect(await html(containerSelector)).toBe( + '
vapor compA
', + ) + }) + }) + describe('transition with v-show', () => { test( 'named transition with v-show', diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index 4855098243b..60595314ee2 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -7,6 +7,7 @@ import { VaporTransition, createIf, template, + defineVaporAsyncComponent, } from 'vue' const show = ref(true) const toggle = ref(true) @@ -90,6 +91,10 @@ const viewInOut = shallowRef(SimpleOne) function changeViewInOut() { viewInOut.value = viewInOut.value === SimpleOne ? Two : SimpleOne } + +const AsyncComp = defineVaporAsyncComponent(() => { + return new Promise(resolve => setTimeout(() => resolve(VaporCompA), 50)) +})