Skip to content

Commit 771379c

Browse files
committed
feat(vapor): suspense interop with Vapor components
1 parent 5036f91 commit 771379c

File tree

7 files changed

+195
-89
lines changed

7 files changed

+195
-89
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, isFunction, isObject } from '@vue/shared'
30-
import { version } from '.'
30+
import { type SuspenseBoundary, version } from '.'
3131
import { installAppCompatProperties } from './compat/global'
3232
import type { NormalizedPropsOptions } from './componentProps'
3333
import type { ObjectEmitsOptions } from './componentEmits'
@@ -182,6 +182,8 @@ export interface VaporInteropInterface {
182182
container: any,
183183
anchor: any,
184184
parentComponent: ComponentInternalInstance | null,
185+
parentSuspense: SuspenseBoundary | null,
186+
isSingleRoot?: boolean,
185187
): GenericComponentInstance // VaporComponentInstance
186188
update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
187189
unmount(vnode: VNode, doRemove?: boolean): void

packages/runtime-core/src/component.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,19 @@ export interface GenericComponentInstance {
438438
* @internal
439439
*/
440440
suspense: SuspenseBoundary | null
441+
/**
442+
* suspense pending batch id
443+
* @internal
444+
*/
445+
suspenseId: number
446+
/**
447+
* @internal
448+
*/
449+
asyncDep: Promise<any> | null
450+
/**
451+
* @internal
452+
*/
453+
asyncResolved: boolean
441454

442455
// lifecycle
443456
/**

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

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ function createSuspenseBoundary(
692692
if (isInPendingSuspense) {
693693
suspense.deps++
694694
}
695-
const hydratedEl = instance.vnode.el
695+
const hydratedEl = instance.vapor ? null : instance.vnode.el
696696
instance
697697
.asyncDep!.catch(err => {
698698
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
@@ -709,37 +709,44 @@ function createSuspenseBoundary(
709709
}
710710
// retry from this component
711711
instance.asyncResolved = true
712-
const { vnode } = instance
713-
if (__DEV__) {
714-
pushWarningContext(vnode)
715-
}
716-
handleSetupResult(instance, asyncSetupResult, false)
717-
if (hydratedEl) {
718-
// vnode may have been replaced if an update happened before the
719-
// async dep is resolved.
720-
vnode.el = hydratedEl
721-
}
722-
const placeholder = !hydratedEl && instance.subTree.el
723-
setupRenderEffect(
724-
instance,
725-
vnode,
726-
// component may have been moved before resolve.
727-
// if this is not a hydration, instance.subTree will be the comment
728-
// placeholder.
729-
parentNode(hydratedEl || instance.subTree.el!)!,
730-
// anchor will not be used if this is hydration, so only need to
731-
// consider the comment placeholder case.
732-
hydratedEl ? null : next(instance.subTree),
733-
suspense,
734-
namespace,
735-
optimized,
736-
)
737-
if (placeholder) {
738-
remove(placeholder)
739-
}
740-
updateHOCHostEl(instance, vnode.el)
741-
if (__DEV__) {
742-
popWarningContext()
712+
713+
// vapor component
714+
if (instance.vapor) {
715+
// @ts-expect-error
716+
setupRenderEffect(asyncSetupResult)
717+
} else {
718+
const { vnode } = instance
719+
if (__DEV__) {
720+
pushWarningContext(vnode)
721+
}
722+
handleSetupResult(instance, asyncSetupResult, false)
723+
if (hydratedEl) {
724+
// vnode may have been replaced if an update happened before the
725+
// async dep is resolved.
726+
vnode.el = hydratedEl
727+
}
728+
const placeholder = !hydratedEl && instance.subTree.el
729+
setupRenderEffect(
730+
instance,
731+
vnode,
732+
// component may have been moved before resolve.
733+
// if this is not a hydration, instance.subTree will be the comment
734+
// placeholder.
735+
parentNode(hydratedEl || instance.subTree.el!)!,
736+
// anchor will not be used if this is hydration, so only need to
737+
// consider the comment placeholder case.
738+
hydratedEl ? null : next(instance.subTree),
739+
suspense,
740+
namespace,
741+
optimized,
742+
)
743+
if (placeholder) {
744+
remove(placeholder)
745+
}
746+
updateHOCHostEl(instance, vnode.el)
747+
if (__DEV__) {
748+
popWarningContext()
749+
}
743750
}
744751
// only decrease deps count if suspense is not already resolved
745752
if (isInPendingSuspense && --suspense.deps === 0) {

packages/runtime-core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,7 @@ export { startMeasure, endMeasure } from './profiling'
557557
* @internal
558558
*/
559559
export { initFeatureFlags } from './featureFlags'
560+
/**
561+
* @internal
562+
*/
563+
export { getComponentName } from './component'

packages/runtime-core/src/renderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,7 @@ function baseCreateRenderer(
11691169
container,
11701170
anchor,
11711171
parentComponent,
1172+
parentSuspense,
11721173
)
11731174
} else {
11741175
getVaporInterface(parentComponent, n2).update(

packages/runtime-vapor/src/component.ts

Lines changed: 102 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
currentInstance,
1616
endMeasure,
1717
expose,
18+
getComponentName,
1819
nextUid,
1920
popWarningContext,
2021
pushWarningContext,
@@ -35,7 +36,13 @@ import {
3536
resetTracking,
3637
unref,
3738
} from '@vue/reactivity'
38-
import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
39+
import {
40+
EMPTY_OBJ,
41+
invokeArrayFns,
42+
isFunction,
43+
isPromise,
44+
isString,
45+
} from '@vue/shared'
3946
import {
4047
type DynamicPropsSource,
4148
type RawProps,
@@ -137,6 +144,7 @@ export function createComponent(
137144
appContext: GenericAppContext = (currentInstance &&
138145
currentInstance.appContext) ||
139146
emptyContext,
147+
parentSuspense?: SuspenseBoundary | null,
140148
): VaporComponentInstance {
141149
const _insertionParent = insertionParent
142150
const _insertionAnchor = insertionAnchor
@@ -180,6 +188,7 @@ export function createComponent(
180188
rawProps as RawProps,
181189
rawSlots as RawSlots,
182190
appContext,
191+
parentSuspense,
183192
)
184193

185194
if (__DEV__) {
@@ -207,56 +216,24 @@ export function createComponent(
207216
]) || EMPTY_OBJ
208217
: EMPTY_OBJ
209218

210-
if (__DEV__ && !isBlock(setupResult)) {
211-
if (isFunction(component)) {
212-
warn(`Functional vapor component must return a block directly.`)
213-
instance.block = []
214-
} else if (!component.render) {
219+
const isAsyncSetup = isPromise(setupResult)
220+
if (__FEATURE_SUSPENSE__ && isAsyncSetup) {
221+
// async setup returned Promise.
222+
// bail here and wait for re-entry.
223+
instance.asyncDep = setupResult
224+
if (__DEV__ && !instance.suspense) {
225+
const name = getComponentName(component, true) ?? 'Anonymous'
215226
warn(
216-
`Vapor component setup() returned non-block value, and has no render function.`,
227+
`Component <${name}>: setup function returned a promise, but no ` +
228+
`<Suspense> boundary was found in the parent component tree. ` +
229+
`A component with async setup() must be nested in a <Suspense> ` +
230+
`in order to be rendered.`,
217231
)
218-
instance.block = []
219-
} else {
220-
instance.devtoolsRawSetupState = setupResult
221-
// TODO make the proxy warn non-existent property access during dev
222-
instance.setupState = proxyRefs(setupResult)
223-
devRender(instance)
224-
225-
// HMR
226-
if (component.__hmrId) {
227-
registerHMR(instance)
228-
instance.isSingleRoot = isSingleRoot
229-
instance.hmrRerender = hmrRerender.bind(null, instance)
230-
instance.hmrReload = hmrReload.bind(null, instance)
231-
}
232-
}
233-
} else {
234-
// component has a render function but no setup function
235-
// (typically components with only a template and no state)
236-
if (!setupFn && component.render) {
237-
instance.block = callWithErrorHandling(
238-
component.render,
239-
instance,
240-
ErrorCodes.RENDER_FUNCTION,
241-
)
242-
} else {
243-
// in prod result can only be block
244-
instance.block = setupResult as Block
245232
}
246233
}
247234

248-
// single root, inherit attrs
249-
if (
250-
instance.hasFallthrough &&
251-
component.inheritAttrs !== false &&
252-
instance.block instanceof Element &&
253-
Object.keys(instance.attrs).length
254-
) {
255-
renderEffect(() => {
256-
isApplyingFallthroughProps = true
257-
setDynamicProps(instance.block as Element, [instance.attrs])
258-
isApplyingFallthroughProps = false
259-
})
235+
if (!isAsyncSetup) {
236+
handleSetupResult(setupResult, component, instance, isSingleRoot, setupFn)
260237
}
261238

262239
resetTracking()
@@ -269,7 +246,7 @@ export function createComponent(
269246

270247
onScopeDispose(() => unmountComponent(instance), true)
271248

272-
if (!isHydrating && _insertionParent) {
249+
if (!isHydrating && _insertionParent && !isAsyncSetup) {
273250
insert(instance.block, _insertionParent, _insertionAnchor)
274251
}
275252

@@ -342,6 +319,9 @@ export class VaporComponentInstance implements GenericComponentInstance {
342319
ids: [string, number, number]
343320
// for suspense
344321
suspense: SuspenseBoundary | null
322+
suspenseId: number
323+
asyncDep: Promise<any> | null
324+
asyncResolved: boolean
345325

346326
hasFallthrough: boolean
347327

@@ -380,6 +360,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
380360
rawProps?: RawProps | null,
381361
rawSlots?: RawSlots | null,
382362
appContext?: GenericAppContext,
363+
suspense?: SuspenseBoundary | null,
383364
) {
384365
this.vapor = true
385366
this.uid = nextUid()
@@ -403,12 +384,13 @@ export class VaporComponentInstance implements GenericComponentInstance {
403384
this.emit = emit.bind(null, this)
404385
this.expose = expose.bind(null, this)
405386
this.refs = EMPTY_OBJ
406-
this.emitted =
407-
this.exposed =
408-
this.exposeProxy =
409-
this.propsDefaults =
410-
this.suspense =
411-
null
387+
this.emitted = this.exposed = this.exposeProxy = this.propsDefaults = null
388+
389+
// suspense related
390+
this.suspense = suspense!
391+
this.suspenseId = suspense ? suspense.pendingId : 0
392+
this.asyncDep = null
393+
this.asyncResolved = false
412394

413395
this.isMounted =
414396
this.isUnmounted =
@@ -545,3 +527,70 @@ export function getExposed(
545527
)
546528
}
547529
}
530+
531+
export function handleSetupResult(
532+
setupResult: any,
533+
component: VaporComponent,
534+
instance: VaporComponentInstance,
535+
isSingleRoot: boolean | undefined,
536+
setupFn: VaporSetupFn | undefined,
537+
): void {
538+
if (__DEV__) {
539+
pushWarningContext(instance)
540+
}
541+
if (__DEV__ && !isBlock(setupResult)) {
542+
if (isFunction(component)) {
543+
warn(`Functional vapor component must return a block directly.`)
544+
instance.block = []
545+
} else if (!component.render) {
546+
warn(
547+
`Vapor component setup() returned non-block value, and has no render function.`,
548+
)
549+
instance.block = []
550+
} else {
551+
instance.devtoolsRawSetupState = setupResult
552+
// TODO make the proxy warn non-existent property access during dev
553+
instance.setupState = proxyRefs(setupResult)
554+
devRender(instance)
555+
556+
// HMR
557+
if (component.__hmrId) {
558+
registerHMR(instance)
559+
instance.isSingleRoot = isSingleRoot
560+
instance.hmrRerender = hmrRerender.bind(null, instance)
561+
instance.hmrReload = hmrReload.bind(null, instance)
562+
}
563+
}
564+
} else {
565+
// component has a render function but no setup function
566+
// (typically components with only a template and no state)
567+
if (!setupFn && component.render) {
568+
instance.block = callWithErrorHandling(
569+
component.render,
570+
instance,
571+
ErrorCodes.RENDER_FUNCTION,
572+
)
573+
} else {
574+
// in prod result can only be block
575+
instance.block = setupResult as Block
576+
}
577+
}
578+
579+
// single root, inherit attrs
580+
if (
581+
instance.hasFallthrough &&
582+
component.inheritAttrs !== false &&
583+
instance.block instanceof Element &&
584+
Object.keys(instance.attrs).length
585+
) {
586+
renderEffect(() => {
587+
isApplyingFallthroughProps = true
588+
setDynamicProps(instance.block as Element, [instance.attrs])
589+
isApplyingFallthroughProps = false
590+
})
591+
}
592+
593+
if (__DEV__) {
594+
popWarningContext()
595+
}
596+
}

0 commit comments

Comments
 (0)