Skip to content

Commit 8d77c1f

Browse files
committed
wip: save
1 parent 2696f14 commit 8d77c1f

File tree

5 files changed

+319
-96
lines changed

5 files changed

+319
-96
lines changed

packages/runtime-core/src/apiAsyncComponent.ts

+138-96
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { ComponentPublicInstance } from './componentPublicInstance'
1212
import { type VNode, createVNode } from './vnode'
1313
import { defineComponent } from './apiDefineComponent'
1414
import { warn } from './warning'
15-
import { ref } from '@vue/reactivity'
15+
import { type Ref, ref } from '@vue/reactivity'
1616
import { ErrorCodes, handleError } from './errorHandling'
1717
import { isKeepAlive } from './components/KeepAlive'
1818
import { markAsyncBoundary } from './helpers/useId'
@@ -24,10 +24,10 @@ export type AsyncComponentLoader<T = any> = () => Promise<
2424
AsyncComponentResolveResult<T>
2525
>
2626

27-
export interface AsyncComponentOptions<T = any> {
27+
export interface AsyncComponentOptions<T = any, C = any> {
2828
loader: AsyncComponentLoader<T>
29-
loadingComponent?: Component
30-
errorComponent?: Component
29+
loadingComponent?: C
30+
errorComponent?: C
3131
delay?: number
3232
timeout?: number
3333
suspensible?: boolean
@@ -46,75 +46,20 @@ export const isAsyncWrapper = (i: GenericComponentInstance | VNode): boolean =>
4646
/*! #__NO_SIDE_EFFECTS__ */
4747
export function defineAsyncComponent<
4848
T extends Component = { new (): ComponentPublicInstance },
49-
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
50-
if (isFunction(source)) {
51-
source = { loader: source }
52-
}
53-
49+
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T, Component>): T {
5450
const {
55-
loader,
56-
loadingComponent,
57-
errorComponent,
58-
delay = 200,
59-
hydrate: hydrateStrategy,
60-
timeout, // undefined = never times out
61-
suspensible = true,
62-
onError: userOnError,
63-
} = source
64-
65-
let pendingRequest: Promise<ConcreteComponent> | null = null
66-
let resolvedComp: ConcreteComponent | undefined
67-
68-
let retries = 0
69-
const retry = () => {
70-
retries++
71-
pendingRequest = null
72-
return load()
73-
}
74-
75-
const load = (): Promise<ConcreteComponent> => {
76-
let thisRequest: Promise<ConcreteComponent>
77-
return (
78-
pendingRequest ||
79-
(thisRequest = pendingRequest =
80-
loader()
81-
.catch(err => {
82-
err = err instanceof Error ? err : new Error(String(err))
83-
if (userOnError) {
84-
return new Promise((resolve, reject) => {
85-
const userRetry = () => resolve(retry())
86-
const userFail = () => reject(err)
87-
userOnError(err, userRetry, userFail, retries + 1)
88-
})
89-
} else {
90-
throw err
91-
}
92-
})
93-
.then((comp: any) => {
94-
if (thisRequest !== pendingRequest && pendingRequest) {
95-
return pendingRequest
96-
}
97-
if (__DEV__ && !comp) {
98-
warn(
99-
`Async component loader resolved to undefined. ` +
100-
`If you are using retry(), make sure to return its return value.`,
101-
)
102-
}
103-
// interop module default
104-
if (
105-
comp &&
106-
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
107-
) {
108-
comp = comp.default
109-
}
110-
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
111-
throw new Error(`Invalid async component load result: ${comp}`)
112-
}
113-
resolvedComp = comp
114-
return comp
115-
}))
116-
)
117-
}
51+
load,
52+
getResolvedComp,
53+
setPendingRequest,
54+
source: {
55+
loadingComponent,
56+
errorComponent,
57+
delay,
58+
hydrate: hydrateStrategy,
59+
timeout,
60+
suspensible = true,
61+
},
62+
} = createAsyncComponentContext(source)
11863

11964
return defineComponent({
12065
name: 'AsyncComponentWrapper',
@@ -132,28 +77,29 @@ export function defineAsyncComponent<
13277
}
13378
}
13479
: hydrate
135-
if (resolvedComp) {
80+
if (getResolvedComp()) {
13681
doHydrate()
13782
} else {
13883
load().then(() => !instance.isUnmounted && doHydrate())
13984
}
14085
},
14186

14287
get __asyncResolved() {
143-
return resolvedComp
88+
return getResolvedComp()
14489
},
14590

14691
setup() {
14792
const instance = currentInstance as ComponentInternalInstance
14893
markAsyncBoundary(instance)
14994

15095
// already resolved
96+
let resolvedComp = getResolvedComp()
15197
if (resolvedComp) {
15298
return () => createInnerComp(resolvedComp!, instance)
15399
}
154100

155101
const onError = (err: Error) => {
156-
pendingRequest = null
102+
setPendingRequest(null)
157103
handleError(
158104
err,
159105
instance,
@@ -182,27 +128,11 @@ export function defineAsyncComponent<
182128
})
183129
}
184130

185-
const loaded = ref(false)
186-
const error = ref()
187-
const delayed = ref(!!delay)
188-
189-
if (delay) {
190-
setTimeout(() => {
191-
delayed.value = false
192-
}, delay)
193-
}
194-
195-
if (timeout != null) {
196-
setTimeout(() => {
197-
if (!loaded.value && !error.value) {
198-
const err = new Error(
199-
`Async component timed out after ${timeout}ms.`,
200-
)
201-
onError(err)
202-
error.value = err
203-
}
204-
}, timeout)
205-
}
131+
const { loaded, error, delayed } = useAsyncComponentState(
132+
delay,
133+
timeout,
134+
onError,
135+
)
206136

207137
load()
208138
.then(() => {
@@ -223,6 +153,7 @@ export function defineAsyncComponent<
223153
})
224154

225155
return () => {
156+
resolvedComp = getResolvedComp()
226157
if (loaded.value && resolvedComp) {
227158
return createInnerComp(resolvedComp, instance)
228159
} else if (error.value && errorComponent) {
@@ -252,3 +183,114 @@ function createInnerComp(
252183

253184
return vnode
254185
}
186+
187+
type AsyncComponentContext<T, C = ConcreteComponent> = {
188+
load: () => Promise<C>
189+
source: AsyncComponentOptions<T>
190+
getResolvedComp: () => C | undefined
191+
setPendingRequest: (request: Promise<C> | null) => void
192+
}
193+
194+
// shared between core and vapor
195+
export function createAsyncComponentContext<T, C = ConcreteComponent>(
196+
source: AsyncComponentLoader<T> | AsyncComponentOptions<T>,
197+
): AsyncComponentContext<T, C> {
198+
if (isFunction(source)) {
199+
source = { loader: source }
200+
}
201+
202+
const { loader, onError: userOnError } = source
203+
let pendingRequest: Promise<C> | null = null
204+
let resolvedComp: C | undefined
205+
206+
let retries = 0
207+
const retry = () => {
208+
retries++
209+
pendingRequest = null
210+
return load()
211+
}
212+
213+
const load = (): Promise<C> => {
214+
let thisRequest: Promise<C>
215+
return (
216+
pendingRequest ||
217+
(thisRequest = pendingRequest =
218+
loader()
219+
.catch(err => {
220+
err = err instanceof Error ? err : new Error(String(err))
221+
if (userOnError) {
222+
return new Promise((resolve, reject) => {
223+
const userRetry = () => resolve(retry())
224+
const userFail = () => reject(err)
225+
userOnError(err, userRetry, userFail, retries + 1)
226+
})
227+
} else {
228+
throw err
229+
}
230+
})
231+
.then((comp: any) => {
232+
if (thisRequest !== pendingRequest && pendingRequest) {
233+
return pendingRequest
234+
}
235+
if (__DEV__ && !comp) {
236+
warn(
237+
`Async component loader resolved to undefined. ` +
238+
`If you are using retry(), make sure to return its return value.`,
239+
)
240+
}
241+
if (
242+
comp &&
243+
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
244+
) {
245+
comp = comp.default
246+
}
247+
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
248+
throw new Error(`Invalid async component load result: ${comp}`)
249+
}
250+
resolvedComp = comp
251+
return comp
252+
}))
253+
)
254+
}
255+
256+
return {
257+
load,
258+
source,
259+
getResolvedComp: () => resolvedComp,
260+
setPendingRequest: (request: Promise<C> | null) =>
261+
(pendingRequest = request),
262+
}
263+
}
264+
265+
// shared between core and vapor
266+
export const useAsyncComponentState = (
267+
delay: number | undefined,
268+
timeout: number | undefined,
269+
onError: (err: Error) => void,
270+
): {
271+
loaded: Ref<boolean>
272+
error: Ref<Error | undefined>
273+
delayed: Ref<boolean>
274+
} => {
275+
const loaded = ref(false)
276+
const error = ref()
277+
const delayed = ref(!!delay)
278+
279+
if (delay) {
280+
setTimeout(() => {
281+
delayed.value = false
282+
}, delay)
283+
}
284+
285+
if (timeout != null) {
286+
setTimeout(() => {
287+
if (!loaded.value && !error.value) {
288+
const err = new Error(`Async component timed out after ${timeout}ms.`)
289+
onError(err)
290+
error.value = err
291+
}
292+
}, timeout)
293+
}
294+
295+
return { loaded, error, delayed }
296+
}

packages/runtime-core/src/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,14 @@ export { startMeasure, endMeasure } from './profiling'
557557
* @internal
558558
*/
559559
export { initFeatureFlags } from './featureFlags'
560+
/**
561+
* @internal
562+
*/
563+
export {
564+
createAsyncComponentContext,
565+
useAsyncComponentState,
566+
} from './apiAsyncComponent'
567+
/**
568+
* @internal
569+
*/
570+
export { markAsyncBoundary } from './helpers/useId'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { nextTick, ref } from '@vue/runtime-dom'
2+
import { type VaporComponent, createComponent } from '../src/component'
3+
import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
4+
import { makeRender } from './_utils'
5+
import { createIf, template } from '@vue/runtime-vapor'
6+
7+
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
8+
9+
const define = makeRender()
10+
11+
describe('api: defineAsyncComponent', () => {
12+
test('simple usage', async () => {
13+
let resolve: (comp: VaporComponent) => void
14+
const Foo = defineVaporAsyncComponent(
15+
() =>
16+
new Promise(r => {
17+
resolve = r as any
18+
}),
19+
)
20+
21+
const toggle = ref(true)
22+
const { html } = define({
23+
setup() {
24+
return createIf(
25+
() => toggle.value,
26+
() => {
27+
return createComponent(Foo)
28+
},
29+
)
30+
},
31+
}).render()
32+
33+
expect(html()).toBe('<!--async component--><!--if-->')
34+
resolve!(() => template('resolved')())
35+
36+
await timeout()
37+
expect(html()).toBe('resolved<!--async component--><!--if-->')
38+
39+
toggle.value = false
40+
await nextTick()
41+
expect(html()).toBe('<!--if-->')
42+
43+
// already resolved component should update on nextTick
44+
toggle.value = true
45+
await nextTick()
46+
expect(html()).toBe('resolved<!--async component--><!--if-->')
47+
})
48+
})

0 commit comments

Comments
 (0)