import { type Component, type ComponentInternalInstance, type ComponentOptions, type ConcreteComponent, currentInstance, getComponentName, isInSSRComponentSetup, } from './component' import { isFunction, isObject } from '@vue/shared' import type { ComponentPublicInstance } from './componentPublicInstance' import { type VNode, createVNode } from './vnode' import { defineComponent } from './apiDefineComponent' import { onUnmounted } from './apiLifecycle' import { warn } from './warning' import { ref } from '@vue/reactivity' import { ErrorCodes, handleError } from './errorHandling' import { isKeepAlive } from './components/KeepAlive' import { markAsyncBoundary } from './helpers/useId' import { type HydrationStrategy, forEachElement } from './hydrationStrategies' export type AsyncComponentResolveResult = T | { default: T } // es modules export type AsyncComponentLoader = () => Promise< AsyncComponentResolveResult > export interface AsyncComponentOptions { loader: AsyncComponentLoader loadingComponent?: Component errorComponent?: Component delay?: number timeout?: number suspensible?: boolean hydrate?: HydrationStrategy onError?: ( error: Error, retry: () => void, fail: () => void, attempts: number, ) => any } export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean => !!(i.type as ComponentOptions).__asyncLoader /*@__NO_SIDE_EFFECTS__*/ export function defineAsyncComponent< T extends Component = { new (): ComponentPublicInstance }, >(source: AsyncComponentLoader | AsyncComponentOptions): T { if (isFunction(source)) { source = { loader: source } } const { loader, loadingComponent, errorComponent, delay = 200, hydrate: hydrateStrategy, timeout, // undefined = never times out suspensible = true, onError: userOnError, } = source let pendingRequest: Promise | null = null let resolvedComp: ConcreteComponent | undefined let retries = 0 const retry = () => { retries++ pendingRequest = null return load() } const load = (): Promise => { let thisRequest: Promise return ( pendingRequest || (thisRequest = pendingRequest = loader() .catch(err => { err = err instanceof Error ? err : new Error(String(err)) if (userOnError) { return new Promise((resolve, reject) => { const userRetry = () => resolve(retry()) const userFail = () => reject(err) userOnError(err, userRetry, userFail, retries + 1) }) } else { throw err } }) .then((comp: any) => { if (thisRequest !== pendingRequest && pendingRequest) { return pendingRequest } if (__DEV__ && !comp) { warn( `Async component loader resolved to undefined. ` + `If you are using retry(), make sure to return its return value.`, ) } // interop module default if ( comp && (comp.__esModule || comp[Symbol.toStringTag] === 'Module') ) { comp = comp.default } if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { throw new Error(`Invalid async component load result: ${comp}`) } resolvedComp = comp return comp })) ) } return defineComponent({ name: 'AsyncComponentWrapper', __asyncLoader: load, __asyncHydrate(el, instance, hydrate) { let patched = false ;(instance.bu || (instance.bu = [])).push(() => (patched = true)) const performHydrate = () => { // skip hydration if the component has been patched if (patched) { if (__DEV__) { warn( `Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` + `it was updated before lazy hydration performed.`, ) } return } hydrate() } const doHydrate = hydrateStrategy ? () => { const teardown = hydrateStrategy(performHydrate, cb => forEachElement(el, cb), ) if (teardown) { ;(instance.bum || (instance.bum = [])).push(teardown) } } : performHydrate if (resolvedComp) { doHydrate() } else { load().then(() => !instance.isUnmounted && doHydrate()) } }, get __asyncResolved() { return resolvedComp }, setup() { const instance = currentInstance! markAsyncBoundary(instance) // already resolved if (resolvedComp) { return () => createInnerComp(resolvedComp!, instance) } const onError = (err: Error) => { pendingRequest = null handleError( err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER, !errorComponent /* do not throw in dev if user provided error component */, ) } // suspense-controlled or SSR. if ( (__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 }) } const loaded = ref(false) const error = ref() const delayed = ref(!!delay) let timeoutTimer: ReturnType | undefined let delayTimer: ReturnType | undefined onUnmounted(() => { if (timeoutTimer != null) clearTimeout(timeoutTimer) if (delayTimer != null) clearTimeout(delayTimer) }) if (delay) { delayTimer = setTimeout(() => { if (instance.isUnmounted) return delayed.value = false }, delay) } if (timeout != null) { timeoutTimer = setTimeout(() => { if (instance.isUnmounted) return if (!loaded.value && !error.value) { const err = new Error( `Async component timed out after ${timeout}ms.`, ) onError(err) error.value = err } }, timeout) } load() .then(() => { if (instance.isUnmounted) return 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 => { if (instance.isUnmounted) { pendingRequest = null return } onError(err) error.value = err }) return () => { if (loaded.value && resolvedComp) { return createInnerComp(resolvedComp, instance) } else if (error.value && errorComponent) { return createVNode(errorComponent, { error: error.value, }) } else if (loadingComponent && !delayed.value) { return createInnerComp( loadingComponent as ConcreteComponent, instance, ) } } }, }) as T } function createInnerComp( comp: ConcreteComponent, parent: ComponentInternalInstance, ) { const { ref, props, children, ce } = parent.vnode const vnode = createVNode(comp, props, children) // ensure inner component inherits the async wrapper's ref owner vnode.ref = ref // pass the custom element callback on to the inner comp // and remove it from the async wrapper vnode.ce = ce delete parent.vnode.ce return vnode }