Skip to content

Commit 1df5e3b

Browse files
committed
feat: vdom Suspense render vapor components
1 parent 3f3a59d commit 1df5e3b

File tree

9 files changed

+483
-103
lines changed

9 files changed

+483
-103
lines changed

packages/runtime-core/src/apiCreateApp.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { warn } from './warning'
2727
import type { VNode } from './vnode'
2828
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
2929
import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
30-
import { type TransitionHooks, version } from '.'
30+
import { type SuspenseBoundary, type TransitionHooks, version } from '.'
3131
import { installAppCompatProperties } from './compat/global'
3232
import type { NormalizedPropsOptions } from './componentProps'
3333
import type { ObjectEmitsOptions } from './componentEmits'
@@ -187,6 +187,7 @@ export interface VaporInteropInterface {
187187
container: any,
188188
anchor: any,
189189
parentComponent: ComponentInternalInstance | null,
190+
parentSuspense: SuspenseBoundary | null,
190191
): GenericComponentInstance // VaporComponentInstance
191192
update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
192193
unmount(vnode: VNode, doRemove?: boolean): void
@@ -198,6 +199,7 @@ export interface VaporInteropInterface {
198199
container: any,
199200
anchor: any,
200201
parentComponent: ComponentInternalInstance | null,
202+
parentSuspense: SuspenseBoundary | null,
201203
): Node
202204
hydrateSlot(vnode: VNode, node: any): Node
203205
activate(

packages/runtime-core/src/component.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,19 @@ export interface GenericComponentInstance {
461461
* @internal
462462
*/
463463
suspense: SuspenseBoundary | null
464+
/**
465+
* suspense pending batch id
466+
* @internal
467+
*/
468+
suspenseId: number
469+
/**
470+
* @internal
471+
*/
472+
asyncDep: Promise<any> | null
473+
/**
474+
* @internal
475+
*/
476+
asyncResolved: boolean
464477
/**
465478
* `updateTeleportCssVars`
466479
* For updating css vars on contained teleports

packages/runtime-core/src/components/Suspense.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ function createSuspenseBoundary(
706706
if (isInPendingSuspense) {
707707
suspense.deps++
708708
}
709-
const hydratedEl = instance.vnode.el
709+
const hydratedEl = instance.vapor ? null : instance.vnode.el
710710
instance
711711
.asyncDep!.catch(err => {
712712
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
@@ -723,39 +723,45 @@ function createSuspenseBoundary(
723723
}
724724
// retry from this component
725725
instance.asyncResolved = true
726-
const { vnode } = instance
727-
if (__DEV__) {
728-
pushWarningContext(vnode)
729-
}
730-
handleSetupResult(instance, asyncSetupResult, false)
731-
if (hydratedEl) {
732-
// vnode may have been replaced if an update happened before the
733-
// async dep is resolved.
734-
vnode.el = hydratedEl
735-
}
736-
const placeholder = !hydratedEl && instance.subTree.el
737-
setupRenderEffect(
738-
instance,
739-
vnode,
740-
// component may have been moved before resolve.
741-
// if this is not a hydration, instance.subTree will be the comment
742-
// placeholder.
743-
parentNode(hydratedEl || instance.subTree.el!)!,
744-
// anchor will not be used if this is hydration, so only need to
745-
// consider the comment placeholder case.
746-
hydratedEl ? null : next(instance.subTree),
747-
suspense,
748-
namespace,
749-
optimized,
750-
)
751-
if (placeholder) {
752-
// clean up placeholder reference
753-
vnode.placeholder = null
754-
remove(placeholder)
755-
}
756-
updateHOCHostEl(instance, vnode.el)
757-
if (__DEV__) {
758-
popWarningContext()
726+
// vapor component
727+
if (instance.vapor) {
728+
// @ts-expect-error
729+
setupRenderEffect(asyncSetupResult)
730+
} else {
731+
const { vnode } = instance
732+
if (__DEV__) {
733+
pushWarningContext(vnode)
734+
}
735+
handleSetupResult(instance, asyncSetupResult, false)
736+
if (hydratedEl) {
737+
// vnode may have been replaced if an update happened before the
738+
// async dep is resolved.
739+
vnode.el = hydratedEl
740+
}
741+
const placeholder = !hydratedEl && instance.subTree.el
742+
setupRenderEffect(
743+
instance,
744+
vnode,
745+
// component may have been moved before resolve.
746+
// if this is not a hydration, instance.subTree will be the comment
747+
// placeholder.
748+
parentNode(hydratedEl || instance.subTree.el!)!,
749+
// anchor will not be used if this is hydration, so only need to
750+
// consider the comment placeholder case.
751+
hydratedEl ? null : next(instance.subTree),
752+
suspense,
753+
namespace,
754+
optimized,
755+
)
756+
if (placeholder) {
757+
// clean up placeholder reference
758+
vnode.placeholder = null
759+
remove(placeholder)
760+
}
761+
updateHOCHostEl(instance, vnode.el)
762+
if (__DEV__) {
763+
popWarningContext()
764+
}
759765
}
760766
// only decrease deps count if suspense is not already resolved
761767
if (isInPendingSuspense && --suspense.deps === 0) {

packages/runtime-core/src/hydration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ export function createHydrationFunctions(
318318
container,
319319
null,
320320
parentComponent,
321+
parentSuspense,
321322
)
322323
} else {
323324
mountComponent(

packages/runtime-core/src/renderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,7 @@ function baseCreateRenderer(
11861186
container,
11871187
anchor,
11881188
parentComponent,
1189+
parentSuspense,
11891190
)
11901191
}
11911192
} else {
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import { nextTick, reactive } from 'vue'
2+
import { compile, runtimeDom, runtimeVapor } from '../_utils'
3+
4+
describe.todo('VaporSuspense', () => {})
5+
6+
describe('vdom interop', () => {
7+
async function testSuspense(
8+
code: string,
9+
components: Record<string, { code: string; vapor: boolean }> = {},
10+
data: any = {},
11+
{ vapor = false } = {},
12+
) {
13+
const clientComponents: any = {}
14+
for (const key in components) {
15+
const comp = components[key]
16+
let code = comp.code
17+
const isVaporComp = !!comp.vapor
18+
clientComponents[key] = compile(code, data, clientComponents, {
19+
vapor: isVaporComp,
20+
})
21+
}
22+
23+
const clientComp = compile(code, data, clientComponents, {
24+
vapor,
25+
})
26+
27+
const app = (vapor ? runtimeVapor.createVaporApp : runtimeDom.createApp)(
28+
clientComp,
29+
)
30+
app.use(runtimeVapor.vaporInteropPlugin)
31+
32+
const container = document.createElement('div')
33+
document.body.appendChild(container)
34+
app.mount(container)
35+
return { container }
36+
}
37+
38+
function withAsyncScript(code: string) {
39+
return {
40+
code: `
41+
<script vapor>
42+
const data = _data;
43+
const components = _components;
44+
const p = new Promise(r => setTimeout(r, 5))
45+
data.deps.push(p.then(() => Promise.resolve()))
46+
await p
47+
</script>
48+
${code}
49+
`,
50+
vapor: true,
51+
}
52+
}
53+
54+
test('vdom suspense: render vapor components', async () => {
55+
const data = { deps: [] }
56+
const { container } = await testSuspense(
57+
`<script setup>
58+
const components = _components;
59+
</script>
60+
<template>
61+
<Suspense>
62+
<components.VaporChild/>
63+
<template #fallback>
64+
<span>fallback</span>
65+
</template>
66+
</Suspense>
67+
</template>`,
68+
{
69+
VaporChild: withAsyncScript(`<template><div>hi</div></template>`),
70+
},
71+
data,
72+
)
73+
74+
expect(container.innerHTML).toBe(`<span>fallback</span>`)
75+
expect(data.deps.length).toBe(1)
76+
await Promise.all(data.deps)
77+
await nextTick()
78+
expect(container.innerHTML).toBe(`<div>hi</div>`)
79+
})
80+
81+
test('vdom suspense: nested async vapor components', async () => {
82+
const data = { deps: [] }
83+
const { container } = await testSuspense(
84+
`<script setup>
85+
const components = _components;
86+
</script>
87+
<template>
88+
<Suspense>
89+
<components.AsyncOuter/>
90+
<template #fallback>
91+
<span>fallback</span>
92+
</template>
93+
</Suspense>
94+
</template>`,
95+
{
96+
AsyncOuter: withAsyncScript(
97+
`<template><components.AsyncInner/></template>`,
98+
),
99+
AsyncInner: withAsyncScript(`<template><div>inner</div></template>`),
100+
},
101+
data,
102+
)
103+
104+
expect(container.innerHTML).toBe(`<span>fallback</span>`)
105+
106+
await data.deps[0]
107+
await nextTick()
108+
expect(container.innerHTML).toBe(`<span>fallback</span>`)
109+
110+
await Promise.all(data.deps)
111+
await nextTick()
112+
expect(container.innerHTML).toBe(`<div>inner</div>`)
113+
})
114+
115+
test('vdom suspense: content update before suspense resolve', async () => {
116+
const data = reactive({ msg: 'foo', deps: [] })
117+
const { container } = await testSuspense(
118+
`<script setup>
119+
const data = _data;
120+
const components = _components;
121+
</script>
122+
<template>
123+
<Suspense>
124+
<components.VaporChild/>
125+
<template #fallback>
126+
<span>fallback {{data.msg}}</span>
127+
</template>
128+
</Suspense>
129+
</template>`,
130+
{
131+
VaporChild: withAsyncScript(
132+
`<template><div>{{data.msg}}</div></template>`,
133+
),
134+
},
135+
data,
136+
)
137+
138+
expect(container.innerHTML).toBe(`<span>fallback foo</span>`)
139+
140+
data.msg = 'bar'
141+
await nextTick()
142+
expect(container.innerHTML).toBe(`<span>fallback bar</span>`)
143+
144+
await Promise.all(data.deps)
145+
await nextTick()
146+
expect(container.innerHTML).toBe(`<div>bar</div>`)
147+
})
148+
149+
test('vdom suspense: unmount before suspense resolve', async () => {
150+
const data = reactive({ show: true, deps: [] })
151+
const { container } = await testSuspense(
152+
`<script setup>
153+
const data = _data;
154+
const components = _components;
155+
</script>
156+
<template>
157+
<Suspense>
158+
<components.VaporChild v-if="data.show"/>
159+
<template #fallback>
160+
<span>fallback</span>
161+
</template>
162+
</Suspense>
163+
</template>`,
164+
{
165+
VaporChild: withAsyncScript(`<template><div>child</div></template>`),
166+
},
167+
data,
168+
)
169+
170+
expect(container.innerHTML).toBe(`<span>fallback</span>`)
171+
172+
data.show = false
173+
await nextTick()
174+
expect(container.innerHTML).toBe(`<!--v-if-->`)
175+
176+
await Promise.all(data.deps)
177+
await nextTick()
178+
expect(container.innerHTML).toBe(`<!--v-if-->`)
179+
})
180+
181+
test('vdom suspense: unmount suspense after resolve', async () => {
182+
const data = reactive({ show: true, deps: [] })
183+
const { container } = await testSuspense(
184+
`<script setup>
185+
const data = _data;
186+
const components = _components;
187+
</script>
188+
<template>
189+
<Suspense v-if="data.show">
190+
<components.VaporChild/>
191+
<template #fallback>
192+
<span>fallback</span>
193+
</template>
194+
</Suspense>
195+
</template>`,
196+
{
197+
VaporChild: withAsyncScript(`<template><div>child</div></template>`),
198+
},
199+
data,
200+
)
201+
202+
expect(container.innerHTML).toBe(`<span>fallback</span>`)
203+
204+
await Promise.all(data.deps)
205+
await nextTick()
206+
expect(container.innerHTML).toBe(`<div>child</div>`)
207+
208+
data.show = false
209+
await nextTick()
210+
expect(container.innerHTML).toBe(`<!--v-if-->`)
211+
})
212+
213+
test('vdom suspense: unmount suspense before resolve', async () => {
214+
const data = reactive({ show: true, deps: [] })
215+
const { container } = await testSuspense(
216+
`<script setup>
217+
const data = _data;
218+
const components = _components;
219+
</script>
220+
<template>
221+
<Suspense v-if="data.show">
222+
<components.VaporChild/>
223+
<template #fallback>
224+
<span>fallback</span>
225+
</template>
226+
</Suspense>
227+
</template>`,
228+
{
229+
VaporChild: withAsyncScript(`<template><div>child</div></template>`),
230+
},
231+
data,
232+
)
233+
234+
expect(container.innerHTML).toBe(`<span>fallback</span>`)
235+
236+
data.show = false
237+
await nextTick()
238+
expect(container.innerHTML).toBe(`<!--v-if-->`)
239+
240+
await Promise.all(data.deps)
241+
await nextTick()
242+
expect(container.innerHTML).toBe(`<!--v-if-->`)
243+
})
244+
})

0 commit comments

Comments
 (0)