import { Component, ConcreteComponent, currentInstance, ComponentInternalInstance, isInSSRComponentSetup, ComponentOptions } from './component' import { isFunction, isObject } from '@vue/shared' import { ComponentPublicInstance } from './componentPublicInstance' import { createVNode, VNode } from './vnode' import { defineComponent } from './apiDefineComponent' import { warn } from './warning' import { ref } from '@vue/reactivity' import { handleError, ErrorCodes } from './errorHandling' import { isKeepAlive } from './components/KeepAlive' import { queueJob } from './scheduler' 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 onError?: ( error: Error, retry: () => void, fail: () => void, attempts: number ) => any } export const isAsyncWrapper = (i: ComponentInternalInstance | VNode): boolean => !!(i.type as ComponentOptions).__asyncLoader export function defineAsyncComponent< T extends Component = { new (): ComponentPublicInstance } >(source: AsyncComponentLoader | AsyncComponentOptions): T { if (isFunction(source)) { source = { loader: source } } const { loader, loadingComponent, errorComponent, delay = 200, 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, get __asyncResolved() { return resolvedComp }, setup() { const instance = currentInstance! // 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) if (delay) { setTimeout(() => { delayed.value = false }, delay) } if (timeout != null) { setTimeout(() => { if (!loaded.value && !error.value) { const err = new Error( `Async component timed out after ${timeout}ms.` ) onError(err) error.value = err } }, 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 queueJob(instance.parent.update) } }) .catch(err => { onError(err) error.value = err }) return () => { if (loaded.value && resolvedComp) { return createInnerComp(resolvedComp, instance) } else if (error.value && errorComponent) { return createVNode(errorComponent as ConcreteComponent, { error: error.value }) } else if (loadingComponent && !delayed.value) { return createVNode(loadingComponent as ConcreteComponent) } } } }) as T } function createInnerComp( comp: ConcreteComponent, { vnode: { ref, props, children, shapeFlag }, parent }: ComponentInternalInstance ) { const vnode = createVNode(comp, props, children) // ensure inner component inherits the async wrapper's ref owner vnode.ref = ref return vnode }