vue3-yuanma/packages/runtime-core/src/apiAsyncComponent.ts

221 lines
6.0 KiB
TypeScript

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 = Component> = T | { default: T } // es modules
export type AsyncComponentLoader<T = any> = () => Promise<
AsyncComponentResolveResult<T>
>
export interface AsyncComponentOptions<T = any> {
loader: AsyncComponentLoader<T>
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<T> | AsyncComponentOptions<T>): 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<ConcreteComponent> | null = null
let resolvedComp: ConcreteComponent | undefined
let retries = 0
const retry = () => {
retries++
pendingRequest = null
return load()
}
const load = (): Promise<ConcreteComponent> => {
let thisRequest: Promise<ConcreteComponent>
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 } }: ComponentInternalInstance
) {
const vnode = createVNode(comp, props, children)
// ensure inner component inherits the async wrapper's ref owner
vnode.ref = ref
return vnode
}