test.todo('buffer mounted/updated hooks & watch callbacks')
+ test.todo('onResolve')
+
test.todo('content update before suspense resolve')
test.todo('unmount before suspense resolve')
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.`
+ : ``)
)
}
}
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
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<T>(
source: WatcherSource<T>,
cb: (newValue: T, oldValue: T, onCleanup: CleanupRegistrator) => any,
options?: WatchOptions
): StopHandle
+// overload #3: array of multiple sources + cb
export function watch<T extends WatcherSource<unknown>[]>(
sources: T,
cb: (
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): StopHandle {
const instance = currentInstance
+ const suspense = currentSuspense
let getter: Function
if (isArray(source)) {
flush === 'sync'
? invoke
: flush === 'pre'
- ? (job: () => void) => {
+ ? (job: () => any) => {
if (!instance || instance.vnode.el != null) {
queueJob(job)
} else {
job()
}
}
- : queuePostFlushCb
+ : (job: () => any) => queuePostRenderEffect(job, suspense)
const runner = effect(getter, {
lazy: true,
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
}
isArray,
isObject
} from '@vue/shared'
+import { SuspenseBoundary } from './suspense'
export type Data = { [key: string]: unknown }
}
export let currentInstance: ComponentInternalInstance | null = null
+export let currentSuspense: SuspenseBoundary | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance
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
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
+ currentSuspense = parentSuspense
const setupResult = callWithErrorHandling(
setup,
instance,
[propsProxy, setupContext]
)
currentInstance = null
+ currentSuspense = null
if (
setupResult &&
}
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
}`
)
}
- 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) {
// support for 2.x options
if (__FEATURE_OPTIONS__) {
currentInstance = instance
+ currentSuspense = parentSuspense
applyOptions(instance, Component)
currentInstance = null
+ currentSuspense = null
}
if (instance.renderContext === EMPTY_OBJ) {
}
}
-function queuePostEffect(
+export function queuePostRenderEffect(
fn: Function | Function[],
suspense: SuspenseBoundary<any, any> | null
) {
}
hostInsert(el, container, anchor)
if (props != null && props.vnodeMounted != null) {
- queuePostEffect(() => {
+ queuePostRenderEffect(() => {
invokeDirectiveHook(props.vnodeMounted, parentComponent, vnode)
}, parentSuspense)
}
}
if (newProps.vnodeUpdated != null) {
- queuePostEffect(() => {
+ queuePostRenderEffect(() => {
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1)
}, parentSuspense)
}
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)
// 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
parentSuspense.deps--
// retry from this component
instance.asyncResolved = true
- handleSetupResult(instance, asyncSetupResult)
+ handleSetupResult(instance, asyncSetupResult, parentSuspense)
setupRenderEffect(
instance,
parentSuspense,
initialVNode.el = subTree.el
// mounted hook
if (instance.m !== null) {
- queuePostEffect(instance.m, parentSuspense)
+ queuePostRenderEffect(instance.m, parentSuspense)
}
mounted = true
} else {
}
// upated hook
if (instance.u !== null) {
- queuePostEffect(instance.u, parentSuspense)
+ queuePostRenderEffect(instance.u, parentSuspense)
}
if (__DEV__) {
}
if (props != null && props.vnodeUnmounted != null) {
- queuePostEffect(() => {
+ queuePostRenderEffect(() => {
invokeDirectiveHook(props.vnodeUnmounted, parentComponent, vnode)
}, parentSuspense)
}
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)
}
export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol()
export interface SuspenseBoundary<
- HostNode,
- HostElement,
+ HostNode = any,
+ HostElement = any,
HostVNode = VNode<HostNode, HostElement>
> {
vnode: HostVNode