Skip to content

Commit 83e7116

Browse files
committed
feat(runtime-vapor): init async component
1 parent bc04592 commit 83e7116

File tree

4 files changed

+272
-1
lines changed

4 files changed

+272
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import {
2+
type Component,
3+
type ComponentInternalInstance,
4+
currentInstance,
5+
getCurrentInstance,
6+
} from './component'
7+
import { isFunction, isObject } from '@vue/shared'
8+
import { defineComponent } from './apiDefineComponent'
9+
import { warn } from './warning'
10+
import { ref } from '@vue/reactivity'
11+
import { VaporErrorCodes, handleError } from './errorHandling'
12+
// import { isKeepAlive } from './components/KeepAlive'
13+
import { queueJob } from './scheduler'
14+
import { createComponent } from './apiCreateComponent'
15+
import { renderEffect } from './renderEffect'
16+
import { createIf } from './apiCreateIf'
17+
import { template } from '@vue/vapor'
18+
19+
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
20+
21+
export type AsyncComponentLoader<T = any> = () => Promise<
22+
AsyncComponentResolveResult<T>
23+
>
24+
25+
export interface AsyncComponentOptions<T = any> {
26+
loader: AsyncComponentLoader<T>
27+
loadingComponent?: Component
28+
errorComponent?: Component
29+
delay?: number
30+
timeout?: number
31+
suspensible?: boolean
32+
onError?: (
33+
error: Error,
34+
retry: () => void,
35+
fail: () => void,
36+
attempts: number,
37+
) => any
38+
}
39+
40+
export const isAsyncWrapper = (i: ComponentInternalInstance): boolean =>
41+
!!i.component.__asyncLoader
42+
43+
/*! #__NO_SIDE_EFFECTS__ */
44+
export function defineAsyncComponent<T extends Component = Component>(
45+
source: AsyncComponentLoader<T> | AsyncComponentOptions<T>,
46+
): T {
47+
if (isFunction(source)) {
48+
source = { loader: source }
49+
}
50+
51+
const {
52+
loader,
53+
loadingComponent,
54+
errorComponent,
55+
delay = 200,
56+
timeout, // undefined = never times out
57+
suspensible = true,
58+
onError: userOnError,
59+
} = source
60+
61+
let pendingRequest: Promise<Component> | null = null
62+
let resolvedComp: Component | undefined
63+
64+
let retries = 0
65+
const retry = () => {
66+
retries++
67+
pendingRequest = null
68+
return load()
69+
}
70+
71+
const load = (): Promise<Component> => {
72+
let thisRequest: Promise<Component>
73+
return (
74+
pendingRequest ||
75+
(thisRequest = pendingRequest =
76+
loader()
77+
.catch(err => {
78+
err = err instanceof Error ? err : new Error(String(err))
79+
if (userOnError) {
80+
return new Promise((resolve, reject) => {
81+
const userRetry = () => resolve(retry())
82+
const userFail = () => reject(err)
83+
userOnError(err, userRetry, userFail, retries + 1)
84+
})
85+
} else {
86+
throw err
87+
}
88+
})
89+
.then((comp: any) => {
90+
if (thisRequest !== pendingRequest && pendingRequest) {
91+
return pendingRequest
92+
}
93+
if (__DEV__ && !comp) {
94+
warn(
95+
`Async component loader resolved to undefined. ` +
96+
`If you are using retry(), make sure to return its return value.`,
97+
)
98+
}
99+
// interop module default
100+
if (
101+
comp &&
102+
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
103+
) {
104+
comp = comp.default
105+
}
106+
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
107+
throw new Error(`Invalid async component load result: ${comp}`)
108+
}
109+
resolvedComp = comp
110+
return comp
111+
}))
112+
)
113+
}
114+
115+
return defineComponent({
116+
name: 'AsyncComponentWrapper',
117+
118+
__asyncLoader: load,
119+
120+
get __asyncResolved() {
121+
return resolvedComp
122+
},
123+
124+
setup() {
125+
const instance = currentInstance!
126+
127+
// already resolved
128+
if (resolvedComp) {
129+
return createInnerComp(resolvedComp!, instance)
130+
}
131+
132+
const onError = (err: Error) => {
133+
pendingRequest = null
134+
handleError(
135+
err,
136+
instance,
137+
VaporErrorCodes.ASYNC_COMPONENT_LOADER,
138+
!errorComponent /* do not throw in dev if user provided error component */,
139+
)
140+
}
141+
142+
// TODO: handle suspense and SSR.
143+
// suspense-controlled or SSR.
144+
// if (
145+
// (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) ||
146+
// (__SSR__ && isInSSRComponentSetup)
147+
// ) {
148+
// return load()
149+
// .then(comp => {
150+
// return () => createInnerComp(comp, instance)
151+
// })
152+
// .catch(err => {
153+
// onError(err)
154+
// return () =>
155+
// errorComponent
156+
// ? createVNode(errorComponent as ConcreteComponent, {
157+
// error: err,
158+
// })
159+
// : null
160+
// })
161+
// }
162+
163+
const loaded = ref(false)
164+
const error = ref()
165+
const delayed = ref(!!delay)
166+
167+
if (delay) {
168+
setTimeout(() => {
169+
delayed.value = false
170+
}, delay)
171+
}
172+
173+
if (timeout != null) {
174+
setTimeout(() => {
175+
if (!loaded.value && !error.value) {
176+
const err = new Error(
177+
`Async component timed out after ${timeout}ms.`,
178+
)
179+
onError(err)
180+
error.value = err
181+
}
182+
}, timeout)
183+
}
184+
185+
load()
186+
.then(() => {
187+
loaded.value = true
188+
// TODO: handle keep-alive.
189+
// if (instance.parent && isKeepAlive(instance.parent.vnode)) {
190+
// // parent is keep-alive, force update so the loaded component's
191+
// // name is taken into account
192+
// queueJob(instance.parent.update)
193+
// }
194+
})
195+
.catch(err => {
196+
onError(err)
197+
error.value = err
198+
})
199+
200+
// if (loaded.value && resolvedComp) {
201+
// return createInnerComp(resolvedComp, instance)
202+
// } else if (error.value && errorComponent) {
203+
// return createComponent(errorComponent, [{ error: () => error.value }])
204+
// } else if (loadingComponent && !delayed.value) {
205+
// return createComponent(loadingComponent)
206+
// }
207+
return {
208+
loaded,
209+
error,
210+
delayed,
211+
}
212+
},
213+
render(ctx) {
214+
const instance = getCurrentInstance()!
215+
return [
216+
createIf(
217+
() => ctx.loaded && resolvedComp,
218+
() => {
219+
return createInnerComp(resolvedComp!, instance)
220+
},
221+
() =>
222+
createIf(
223+
() => ctx.error && errorComponent,
224+
() =>
225+
createComponent(errorComponent!, [{ error: () => ctx.error }]),
226+
() =>
227+
createIf(
228+
() => loadingComponent && !ctx.delayed,
229+
() => createComponent(loadingComponent!),
230+
),
231+
),
232+
),
233+
]
234+
},
235+
}) as T
236+
}
237+
238+
function createInnerComp(comp: Component, parent: ComponentInternalInstance) {
239+
const { rawProps: props, rawSlots, rawDynamicSlots } = parent
240+
const innerComp = createComponent(comp, props, rawSlots, rawDynamicSlots)
241+
// const vnode = createVNode(comp, props, children)
242+
// // ensure inner component inherits the async wrapper's ref owner
243+
innerComp.refs = parent.refs
244+
// vnode.ref = ref
245+
// // pass the custom element callback on to the inner comp
246+
// // and remove it from the async wrapper
247+
// vnode.ce = ce
248+
// delete parent.vnode.ce
249+
250+
return innerComp
251+
}

packages/runtime-vapor/src/component.ts

+15
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ export interface ObjectComponent extends ComponentInternalOptions {
116116
emits?: EmitsOptions
117117
render?(ctx: any): Block
118118

119+
/**
120+
* marker for AsyncComponentWrapper
121+
* @internal
122+
*/
123+
__asyncLoader?: () => Promise<Component>
124+
/**
125+
* the inner component resolved by the AsyncComponentWrapper
126+
* @internal
127+
*/
128+
__asyncResolved?: Component
129+
119130
name?: string
120131
vapor?: boolean
121132
}
@@ -179,6 +190,8 @@ export interface ComponentInternalInstance {
179190
emit: EmitFn
180191
emitted: Record<string, boolean> | null
181192
attrs: Data
193+
rawSlots: InternalSlots
194+
rawDynamicSlots: DynamicSlots | null
182195
slots: InternalSlots
183196
refs: Data
184197
// exposed properties via expose()
@@ -304,6 +317,8 @@ export function createComponentInstance(
304317
emit: null!,
305318
emitted: null,
306319
attrs: EMPTY_OBJ,
320+
rawSlots: slots || EMPTY_OBJ,
321+
rawDynamicSlots: dynamicSlots || null,
307322
slots: EMPTY_OBJ,
308323
refs: EMPTY_OBJ,
309324

packages/runtime-vapor/src/dom/templateRef.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@vue/shared'
2121
import { warn } from '../warning'
2222
import { queuePostFlushCb } from '../scheduler'
23+
import { isAsyncWrapper } from '../apiAsyncComponent'
2324

2425
export type NodeRef = string | Ref | ((ref: Element) => void)
2526
export type RefEl = Element | ComponentInternalInstance
@@ -36,11 +37,14 @@ export function setRef(
3637
if (!currentInstance) return
3738
const { setupState, isUnmounted } = currentInstance
3839

40+
const isComponent = isVaporComponent(el)
41+
const isAsync = isComponent && isAsyncWrapper(currentInstance)
42+
3943
if (isUnmounted) {
4044
return
4145
}
4246

43-
const refValue = isVaporComponent(el) ? el.exposed || el : el
47+
const refValue = isComponent ? el.exposed || el : el
4448

4549
const refs =
4650
currentInstance.refs === EMPTY_OBJ

packages/runtime-vapor/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
9898
export { setRef } from './dom/templateRef'
9999

100100
export { defineComponent } from './apiDefineComponent'
101+
export { defineAsyncComponent } from './apiAsyncComponent'
101102
export {
102103
type InjectionKey,
103104
inject,

0 commit comments

Comments
 (0)