diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts index 9b1e41f0..16953bee 100644 --- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts +++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts @@ -115,6 +115,8 @@ describe('renderer: suspense', () => { test.todo('buffer mounted/updated hooks & watch callbacks') + test.todo('onResolve') + test.todo('content update before suspense resolve') test.todo('unmount before suspense resolve') diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts index 12f840f8..39782388 100644 --- a/packages/runtime-core/src/apiLifecycle.ts +++ b/packages/runtime-core/src/apiLifecycle.ts @@ -39,7 +39,11 @@ function injectHook( warn( `${apiName} is called when there is no active component instance to be ` + `associated with. ` + - `Lifecycle injection APIs can only be used during execution of setup().` + `Lifecycle injection APIs can only be used during execution of setup().` + + (__FEATURE_SUSPENSE__ + ? ` If you are using async setup(), make sure to register lifecycle ` + + `hooks before the first await statement.` + : ``) ) } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index e2ebd0bd..478a0f66 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -5,16 +5,21 @@ import { Ref, ReactiveEffectOptions } from '@vue/reactivity' -import { queueJob, queuePostFlushCb } from './scheduler' +import { queueJob } from './scheduler' import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared' import { recordEffect } from './apiReactivity' -import { currentInstance, ComponentInternalInstance } from './component' +import { + currentInstance, + ComponentInternalInstance, + currentSuspense +} from './component' import { ErrorCodes, callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' -import { onBeforeMount } from './apiLifecycle' +import { onBeforeUnmount } from './apiLifecycle' +import { queuePostRenderEffect } from './createRenderer' export interface WatchOptions { lazy?: boolean @@ -38,14 +43,17 @@ type SimpleEffect = (onCleanup: CleanupRegistrator) => void const invoke = (fn: Function) => fn() +// overload #1: simple effect export function watch(effect: SimpleEffect, options?: WatchOptions): StopHandle +// overload #2: single source + cb export function watch( source: WatcherSource, cb: (newValue: T, oldValue: T, onCleanup: CleanupRegistrator) => any, options?: WatchOptions ): StopHandle +// overload #3: array of multiple sources + cb export function watch[]>( sources: T, cb: ( @@ -85,6 +93,7 @@ function doWatch( { lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): StopHandle { const instance = currentInstance + const suspense = currentSuspense let getter: Function if (isArray(source)) { @@ -152,7 +161,7 @@ function doWatch( flush === 'sync' ? invoke : flush === 'pre' - ? (job: () => void) => { + ? (job: () => any) => { if (!instance || instance.vnode.el != null) { queueJob(job) } else { @@ -161,7 +170,7 @@ function doWatch( job() } } - : queuePostFlushCb + : (job: () => any) => queuePostRenderEffect(job, suspense) const runner = effect(getter, { lazy: true, @@ -198,7 +207,7 @@ export function instanceWatch( const ctx = this.renderProxy as any const getter = isString(source) ? () => ctx[source] : source.bind(ctx) const stop = watch(getter, cb.bind(ctx), options) - onBeforeMount(stop, this) + onBeforeUnmount(stop, this) return stop } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index e09cd73d..500ac828 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -23,6 +23,7 @@ import { isArray, isObject } from '@vue/shared' +import { SuspenseBoundary } from './suspense' export type Data = { [key: string]: unknown } @@ -206,6 +207,7 @@ export function createComponentInstance( } export let currentInstance: ComponentInternalInstance | null = null +export let currentSuspense: SuspenseBoundary | null = null export const getCurrentInstance: () => ComponentInternalInstance | null = () => currentInstance @@ -216,7 +218,10 @@ export const setCurrentInstance = ( currentInstance = instance } -export function setupStatefulComponent(instance: ComponentInternalInstance) { +export function setupStatefulComponent( + instance: ComponentInternalInstance, + parentSuspense: SuspenseBoundary | null +) { const Component = instance.type as ComponentOptions // 1. create render proxy instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any @@ -231,6 +236,7 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) { setup.length > 1 ? createSetupContext(instance) : null) currentInstance = instance + currentSuspense = parentSuspense const setupResult = callWithErrorHandling( setup, instance, @@ -238,6 +244,7 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) { [propsProxy, setupContext] ) currentInstance = null + currentSuspense = null if ( setupResult && @@ -256,16 +263,17 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) { } return } else { - handleSetupResult(instance, setupResult) + handleSetupResult(instance, setupResult, parentSuspense) } } else { - finishComponentSetup(instance) + finishComponentSetup(instance, parentSuspense) } } export function handleSetupResult( instance: ComponentInternalInstance, - setupResult: unknown + setupResult: unknown, + parentSuspense: SuspenseBoundary | null ) { if (isFunction(setupResult)) { // setup returned an inline render function @@ -281,10 +289,13 @@ export function handleSetupResult( }` ) } - finishComponentSetup(instance) + finishComponentSetup(instance, parentSuspense) } -function finishComponentSetup(instance: ComponentInternalInstance) { +function finishComponentSetup( + instance: ComponentInternalInstance, + parentSuspense: SuspenseBoundary | null +) { const Component = instance.type as ComponentOptions if (!instance.render) { if (__DEV__ && !Component.render) { @@ -299,8 +310,10 @@ function finishComponentSetup(instance: ComponentInternalInstance) { // support for 2.x options if (__FEATURE_OPTIONS__) { currentInstance = instance + currentSuspense = parentSuspense applyOptions(instance, Component) currentInstance = null + currentSuspense = null } if (instance.renderContext === EMPTY_OBJ) { diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index eea24aa3..302b6838 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -78,7 +78,7 @@ function invokeHooks(hooks: Function[], arg?: any) { } } -function queuePostEffect( +export function queuePostRenderEffect( fn: Function | Function[], suspense: SuspenseBoundary | null ) { @@ -357,7 +357,7 @@ export function createRenderer< } hostInsert(el, container, anchor) if (props != null && props.vnodeMounted != null) { - queuePostEffect(() => { + queuePostRenderEffect(() => { invokeDirectiveHook(props.vnodeMounted, parentComponent, vnode) }, parentSuspense) } @@ -508,7 +508,7 @@ export function createRenderer< } if (newProps.vnodeUpdated != null) { - queuePostEffect(() => { + queuePostRenderEffect(() => { invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1) }, parentSuspense) } @@ -700,7 +700,9 @@ export function createRenderer< function resolveSuspense() { const { subTree, fallbackTree, effects, vnode } = suspense // unmount fallback tree - unmount(fallbackTree as HostVNode, parentComponent, suspense, true) + if (fallback.el) { + unmount(fallbackTree as HostVNode, parentComponent, suspense, true) + } // move content from off-dom container to actual container move(subTree as HostVNode, container, anchor) const el = (vnode.el = (subTree as HostVNode).el as HostNode) @@ -895,7 +897,7 @@ export function createRenderer< // setup stateful logic if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { - setupStatefulComponent(instance) + setupStatefulComponent(instance, parentSuspense) } // setup() is async. This component relies on async logic to be resolved @@ -909,7 +911,7 @@ export function createRenderer< parentSuspense.deps-- // retry from this component instance.asyncResolved = true - handleSetupResult(instance, asyncSetupResult) + handleSetupResult(instance, asyncSetupResult, parentSuspense) setupRenderEffect( instance, parentSuspense, @@ -965,7 +967,7 @@ export function createRenderer< initialVNode.el = subTree.el // mounted hook if (instance.m !== null) { - queuePostEffect(instance.m, parentSuspense) + queuePostRenderEffect(instance.m, parentSuspense) } mounted = true } else { @@ -1018,7 +1020,7 @@ export function createRenderer< } // upated hook if (instance.u !== null) { - queuePostEffect(instance.u, parentSuspense) + queuePostRenderEffect(instance.u, parentSuspense) } if (__DEV__) { @@ -1500,7 +1502,7 @@ export function createRenderer< } if (props != null && props.vnodeUnmounted != null) { - queuePostEffect(() => { + queuePostRenderEffect(() => { invokeDirectiveHook(props.vnodeUnmounted, parentComponent, vnode) }, parentSuspense) } @@ -1525,9 +1527,9 @@ export function createRenderer< unmount(subTree, instance, parentSuspense, doRemove) // unmounted hook if (um !== null) { - queuePostEffect(um, parentSuspense) + queuePostRenderEffect(um, parentSuspense) // set unmounted after unmounted hooks are fired - queuePostEffect(() => { + queuePostRenderEffect(() => { instance.isUnmounted = true }, parentSuspense) } diff --git a/packages/runtime-core/src/suspense.ts b/packages/runtime-core/src/suspense.ts index e1d6065f..b264b8a1 100644 --- a/packages/runtime-core/src/suspense.ts +++ b/packages/runtime-core/src/suspense.ts @@ -5,8 +5,8 @@ import { isFunction } from '@vue/shared' export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol() export interface SuspenseBoundary< - HostNode, - HostElement, + HostNode = any, + HostElement = any, HostVNode = VNode > { vnode: HostVNode