From b0366bdfbe0547c4ac31caa5e27515628ad1d5f9 Mon Sep 17 00:00:00 2001 From: daiwei Date: Sun, 2 Mar 2025 14:23:52 +0800 Subject: [PATCH 1/3] wip: save --- .../runtime-core/src/apiAsyncComponent.ts | 97 +++++++++++-------- .../__tests__/e2e/hydration-strat-idle.html | 1 + 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 199b451f66f..2c0ac4b7d20 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -115,26 +115,33 @@ export function defineAsyncComponent< ) } + let performLoad: () => Promise + return defineComponent({ name: 'AsyncComponentWrapper', __asyncLoader: load, __asyncHydrate(el, instance, hydrate) { - const doHydrate = hydrateStrategy - ? () => { - const teardown = hydrateStrategy(hydrate, cb => - forEachElement(el, cb), - ) - if (teardown) { - ;(instance.bum || (instance.bum = [])).push(teardown) - } - } - : hydrate - if (resolvedComp) { - doHydrate() + if (hydrateStrategy) { + let teardown: (() => void) | void + if (resolvedComp) { + teardown = hydrateStrategy(hydrate, cb => forEachElement(el, cb)) + } else { + teardown = hydrateStrategy( + () => performLoad().then(() => !instance.isUnmounted && hydrate()), + cb => forEachElement(el, cb), + ) + } + if (teardown) { + ;(instance.bum || (instance.bum = [])).push(teardown) + } } else { - load().then(() => !instance.isUnmounted && doHydrate()) + if (resolvedComp) { + hydrate() + } else { + load().then(() => !instance.isUnmounted && hydrate()) + } } }, @@ -166,19 +173,25 @@ export function defineAsyncComponent< (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) || (__SSR__ && isInSSRComponentSetup) ) { - return load() - .then(comp => { - return () => createInnerComp(comp, instance) - }) - .catch(err => { - onError(err) - return () => - errorComponent - ? createVNode(errorComponent as ConcreteComponent, { - error: err, - }) - : null - }) + performLoad = () => + load() + .then(comp => { + return () => createInnerComp(comp, instance) + }) + .catch(err => { + onError(err) + return () => + errorComponent + ? createVNode(errorComponent as ConcreteComponent, { + error: err, + }) + : null + }) + + if (!hydrateStrategy) { + return performLoad() + } + return } const loaded = ref(false) @@ -203,19 +216,25 @@ export function defineAsyncComponent< }, timeout) } - load() - .then(() => { - loaded.value = true - if (instance.parent && isKeepAlive(instance.parent.vnode)) { - // parent is keep-alive, force update so the loaded component's - // name is taken into account - instance.parent.update() - } - }) - .catch(err => { - onError(err) - error.value = err - }) + performLoad = () => + load() + .then(() => { + loaded.value = true + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + // parent is keep-alive, force update so the loaded component's + // name is taken into account + instance.parent.update() + } + }) + .catch(err => { + onError(err) + error.value = err + }) + + // lazy perform load if hydrate strategy is present + if (!hydrateStrategy) { + performLoad() + } return () => { if (loaded.value && resolvedComp) { diff --git a/packages/vue/__tests__/e2e/hydration-strat-idle.html b/packages/vue/__tests__/e2e/hydration-strat-idle.html index 23e3aa32a59..d2e71815ffb 100644 --- a/packages/vue/__tests__/e2e/hydration-strat-idle.html +++ b/packages/vue/__tests__/e2e/hydration-strat-idle.html @@ -28,6 +28,7 @@ loader: () => new Promise(resolve => { setTimeout(() => { + debugger console.log('resolve') resolve(Comp) requestIdleCallback(() => { From aea4c6e1dd4752e476b30a3b22d77580b5c796ac Mon Sep 17 00:00:00 2001 From: daiwei Date: Sun, 2 Mar 2025 15:09:08 +0800 Subject: [PATCH 2/3] fix: tests --- packages/runtime-core/src/hydrationStrategies.ts | 7 ++++--- packages/vue/__tests__/e2e/hydration-strat-idle.html | 4 ---- packages/vue/__tests__/e2e/hydrationStrategies.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/runtime-core/src/hydrationStrategies.ts b/packages/runtime-core/src/hydrationStrategies.ts index bad39884830..071ac819b68 100644 --- a/packages/runtime-core/src/hydrationStrategies.ts +++ b/packages/runtime-core/src/hydrationStrategies.ts @@ -18,7 +18,7 @@ const cancelIdleCallback: Window['cancelIdleCallback'] = * listeners. */ export type HydrationStrategy = ( - hydrate: () => void, + hydrate: () => void | Promise, forEachElement: (cb: (el: Element) => any) => void, ) => (() => void) | void @@ -86,11 +86,12 @@ export const hydrateOnInteraction: HydrationStrategyFactory< (hydrate, forEach) => { if (isString(interactions)) interactions = [interactions] let hasHydrated = false - const doHydrate = (e: Event) => { + const doHydrate = async (e: Event) => { if (!hasHydrated) { hasHydrated = true teardown() - hydrate() + // eslint-disable-next-line no-restricted-syntax + await hydrate() // replay event e.target!.dispatchEvent(new (e.constructor as any)(e.type, e)) } diff --git a/packages/vue/__tests__/e2e/hydration-strat-idle.html b/packages/vue/__tests__/e2e/hydration-strat-idle.html index d2e71815ffb..f9131e0c6dd 100644 --- a/packages/vue/__tests__/e2e/hydration-strat-idle.html +++ b/packages/vue/__tests__/e2e/hydration-strat-idle.html @@ -28,12 +28,8 @@ loader: () => new Promise(resolve => { setTimeout(() => { - debugger console.log('resolve') resolve(Comp) - requestIdleCallback(() => { - console.log('busy') - }) }, 10) }), hydrate: hydrateOnIdle(), diff --git a/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts b/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts index 69934d9591e..54c07f3df1e 100644 --- a/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts +++ b/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts @@ -31,8 +31,8 @@ describe('async component hydration strategies', () => { expect(await page().evaluate(() => window.isHydrated)).toBe(false) // wait for hydration await page().waitForFunction(() => window.isHydrated) - // assert message order: hyration should happen after already queued main thread work - expect(messages.slice(1)).toMatchObject(['resolve', 'busy', 'hydrated']) + // assert message order + expect(messages.slice(1)).toMatchObject(['resolve', 'hydrated']) await assertHydrationSuccess() }) From 343247e6307d4e5d7a7172e2efdf5647c65d8659 Mon Sep 17 00:00:00 2001 From: daiwei Date: Sun, 2 Mar 2025 22:28:36 +0800 Subject: [PATCH 3/3] chore: improve --- .../runtime-core/src/apiAsyncComponent.ts | 23 ++++++++++++------- .../runtime-core/src/hydrationStrategies.ts | 19 +++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 2c0ac4b7d20..ac016e5bb65 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -124,15 +124,22 @@ export function defineAsyncComponent< __asyncHydrate(el, instance, hydrate) { if (hydrateStrategy) { - let teardown: (() => void) | void - if (resolvedComp) { - teardown = hydrateStrategy(hydrate, cb => forEachElement(el, cb)) - } else { - teardown = hydrateStrategy( - () => performLoad().then(() => !instance.isUnmounted && hydrate()), - cb => forEachElement(el, cb), - ) + const hydrateWithCallback = (postHydrate?: () => void) => { + if (resolvedComp) { + hydrate() + postHydrate && postHydrate() + } else { + performLoad().then(() => { + if (!instance.isUnmounted) { + hydrate() + postHydrate && postHydrate() + } + }) + } } + let teardown = hydrateStrategy(hydrateWithCallback, cb => + forEachElement(el, cb), + ) if (teardown) { ;(instance.bum || (instance.bum = [])).push(teardown) } diff --git a/packages/runtime-core/src/hydrationStrategies.ts b/packages/runtime-core/src/hydrationStrategies.ts index 071ac819b68..0e99d6be026 100644 --- a/packages/runtime-core/src/hydrationStrategies.ts +++ b/packages/runtime-core/src/hydrationStrategies.ts @@ -18,7 +18,7 @@ const cancelIdleCallback: Window['cancelIdleCallback'] = * listeners. */ export type HydrationStrategy = ( - hydrate: () => void | Promise, + hydrate: (postHydrate?: () => void) => void, forEachElement: (cb: (el: Element) => any) => void, ) => (() => void) | void @@ -29,7 +29,7 @@ export type HydrationStrategyFactory = ( export const hydrateOnIdle: HydrationStrategyFactory = (timeout = 10000) => hydrate => { - const id = requestIdleCallback(hydrate, { timeout }) + const id = requestIdleCallback(() => hydrate(), { timeout }) return () => cancelIdleCallback(id) } @@ -73,8 +73,9 @@ export const hydrateOnMediaQuery: HydrationStrategyFactory = if (mql.matches) { hydrate() } else { - mql.addEventListener('change', hydrate, { once: true }) - return () => mql.removeEventListener('change', hydrate) + const doHydrate = () => hydrate() + mql.addEventListener('change', doHydrate, { once: true }) + return () => mql.removeEventListener('change', doHydrate) } } } @@ -86,14 +87,14 @@ export const hydrateOnInteraction: HydrationStrategyFactory< (hydrate, forEach) => { if (isString(interactions)) interactions = [interactions] let hasHydrated = false - const doHydrate = async (e: Event) => { + const doHydrate = (e: Event) => { if (!hasHydrated) { hasHydrated = true teardown() - // eslint-disable-next-line no-restricted-syntax - await hydrate() - // replay event - e.target!.dispatchEvent(new (e.constructor as any)(e.type, e)) + hydrate(() => { + // replay event + e.target!.dispatchEvent(new (e.constructor as any)(e.type, e)) + }) } } const teardown = () => {