diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 199b451f66f..ac016e5bb65 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -115,26 +115,40 @@ 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) - } + if (hydrateStrategy) { + const hydrateWithCallback = (postHydrate?: () => void) => { + if (resolvedComp) { + hydrate() + postHydrate && postHydrate() + } else { + performLoad().then(() => { + if (!instance.isUnmounted) { + hydrate() + postHydrate && postHydrate() + } + }) } - : hydrate - if (resolvedComp) { - doHydrate() + } + let teardown = hydrateStrategy(hydrateWithCallback, 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 +180,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 +223,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/runtime-core/src/hydrationStrategies.ts b/packages/runtime-core/src/hydrationStrategies.ts index bad39884830..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, + 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) } } } @@ -90,9 +91,10 @@ export const hydrateOnInteraction: HydrationStrategyFactory< if (!hasHydrated) { hasHydrated = true teardown() - 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 = () => { diff --git a/packages/vue/__tests__/e2e/hydration-strat-idle.html b/packages/vue/__tests__/e2e/hydration-strat-idle.html index 23e3aa32a59..f9131e0c6dd 100644 --- a/packages/vue/__tests__/e2e/hydration-strat-idle.html +++ b/packages/vue/__tests__/e2e/hydration-strat-idle.html @@ -30,9 +30,6 @@ setTimeout(() => { 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() })