wip: watcher callback handling inside suspense
This commit is contained in:
parent
51914c76e8
commit
356a01780b
@ -115,6 +115,8 @@ describe('renderer: suspense', () => {
|
|||||||
|
|
||||||
test.todo('buffer mounted/updated hooks & watch callbacks')
|
test.todo('buffer mounted/updated hooks & watch callbacks')
|
||||||
|
|
||||||
|
test.todo('onResolve')
|
||||||
|
|
||||||
test.todo('content update before suspense resolve')
|
test.todo('content update before suspense resolve')
|
||||||
|
|
||||||
test.todo('unmount before suspense resolve')
|
test.todo('unmount before suspense resolve')
|
||||||
|
@ -39,7 +39,11 @@ function injectHook(
|
|||||||
warn(
|
warn(
|
||||||
`${apiName} is called when there is no active component instance to be ` +
|
`${apiName} is called when there is no active component instance to be ` +
|
||||||
`associated with. ` +
|
`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.`
|
||||||
|
: ``)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,21 @@ import {
|
|||||||
Ref,
|
Ref,
|
||||||
ReactiveEffectOptions
|
ReactiveEffectOptions
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { queueJob, queuePostFlushCb } from './scheduler'
|
import { queueJob } from './scheduler'
|
||||||
import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
|
import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
|
||||||
import { recordEffect } from './apiReactivity'
|
import { recordEffect } from './apiReactivity'
|
||||||
import { currentInstance, ComponentInternalInstance } from './component'
|
import {
|
||||||
|
currentInstance,
|
||||||
|
ComponentInternalInstance,
|
||||||
|
currentSuspense
|
||||||
|
} from './component'
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
callWithErrorHandling,
|
callWithErrorHandling,
|
||||||
callWithAsyncErrorHandling
|
callWithAsyncErrorHandling
|
||||||
} from './errorHandling'
|
} from './errorHandling'
|
||||||
import { onBeforeMount } from './apiLifecycle'
|
import { onBeforeUnmount } from './apiLifecycle'
|
||||||
|
import { queuePostRenderEffect } from './createRenderer'
|
||||||
|
|
||||||
export interface WatchOptions {
|
export interface WatchOptions {
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
@ -38,14 +43,17 @@ type SimpleEffect = (onCleanup: CleanupRegistrator) => void
|
|||||||
|
|
||||||
const invoke = (fn: Function) => fn()
|
const invoke = (fn: Function) => fn()
|
||||||
|
|
||||||
|
// overload #1: simple effect
|
||||||
export function watch(effect: SimpleEffect, options?: WatchOptions): StopHandle
|
export function watch(effect: SimpleEffect, options?: WatchOptions): StopHandle
|
||||||
|
|
||||||
|
// overload #2: single source + cb
|
||||||
export function watch<T>(
|
export function watch<T>(
|
||||||
source: WatcherSource<T>,
|
source: WatcherSource<T>,
|
||||||
cb: (newValue: T, oldValue: T, onCleanup: CleanupRegistrator) => any,
|
cb: (newValue: T, oldValue: T, onCleanup: CleanupRegistrator) => any,
|
||||||
options?: WatchOptions
|
options?: WatchOptions
|
||||||
): StopHandle
|
): StopHandle
|
||||||
|
|
||||||
|
// overload #3: array of multiple sources + cb
|
||||||
export function watch<T extends WatcherSource<unknown>[]>(
|
export function watch<T extends WatcherSource<unknown>[]>(
|
||||||
sources: T,
|
sources: T,
|
||||||
cb: (
|
cb: (
|
||||||
@ -85,6 +93,7 @@ function doWatch(
|
|||||||
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
||||||
): StopHandle {
|
): StopHandle {
|
||||||
const instance = currentInstance
|
const instance = currentInstance
|
||||||
|
const suspense = currentSuspense
|
||||||
|
|
||||||
let getter: Function
|
let getter: Function
|
||||||
if (isArray(source)) {
|
if (isArray(source)) {
|
||||||
@ -152,7 +161,7 @@ function doWatch(
|
|||||||
flush === 'sync'
|
flush === 'sync'
|
||||||
? invoke
|
? invoke
|
||||||
: flush === 'pre'
|
: flush === 'pre'
|
||||||
? (job: () => void) => {
|
? (job: () => any) => {
|
||||||
if (!instance || instance.vnode.el != null) {
|
if (!instance || instance.vnode.el != null) {
|
||||||
queueJob(job)
|
queueJob(job)
|
||||||
} else {
|
} else {
|
||||||
@ -161,7 +170,7 @@ function doWatch(
|
|||||||
job()
|
job()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: queuePostFlushCb
|
: (job: () => any) => queuePostRenderEffect(job, suspense)
|
||||||
|
|
||||||
const runner = effect(getter, {
|
const runner = effect(getter, {
|
||||||
lazy: true,
|
lazy: true,
|
||||||
@ -198,7 +207,7 @@ export function instanceWatch(
|
|||||||
const ctx = this.renderProxy as any
|
const ctx = this.renderProxy as any
|
||||||
const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
|
const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
|
||||||
const stop = watch(getter, cb.bind(ctx), options)
|
const stop = watch(getter, cb.bind(ctx), options)
|
||||||
onBeforeMount(stop, this)
|
onBeforeUnmount(stop, this)
|
||||||
return stop
|
return stop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
isArray,
|
isArray,
|
||||||
isObject
|
isObject
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
|
import { SuspenseBoundary } from './suspense'
|
||||||
|
|
||||||
export type Data = { [key: string]: unknown }
|
export type Data = { [key: string]: unknown }
|
||||||
|
|
||||||
@ -206,6 +207,7 @@ export function createComponentInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export let currentInstance: ComponentInternalInstance | null = null
|
export let currentInstance: ComponentInternalInstance | null = null
|
||||||
|
export let currentSuspense: SuspenseBoundary | null = null
|
||||||
|
|
||||||
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
||||||
currentInstance
|
currentInstance
|
||||||
@ -216,7 +218,10 @@ export const setCurrentInstance = (
|
|||||||
currentInstance = instance
|
currentInstance = instance
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
export function setupStatefulComponent(
|
||||||
|
instance: ComponentInternalInstance,
|
||||||
|
parentSuspense: SuspenseBoundary | null
|
||||||
|
) {
|
||||||
const Component = instance.type as ComponentOptions
|
const Component = instance.type as ComponentOptions
|
||||||
// 1. create render proxy
|
// 1. create render proxy
|
||||||
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any
|
instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) as any
|
||||||
@ -231,6 +236,7 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
|||||||
setup.length > 1 ? createSetupContext(instance) : null)
|
setup.length > 1 ? createSetupContext(instance) : null)
|
||||||
|
|
||||||
currentInstance = instance
|
currentInstance = instance
|
||||||
|
currentSuspense = parentSuspense
|
||||||
const setupResult = callWithErrorHandling(
|
const setupResult = callWithErrorHandling(
|
||||||
setup,
|
setup,
|
||||||
instance,
|
instance,
|
||||||
@ -238,6 +244,7 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
|||||||
[propsProxy, setupContext]
|
[propsProxy, setupContext]
|
||||||
)
|
)
|
||||||
currentInstance = null
|
currentInstance = null
|
||||||
|
currentSuspense = null
|
||||||
|
|
||||||
if (
|
if (
|
||||||
setupResult &&
|
setupResult &&
|
||||||
@ -256,16 +263,17 @@ export function setupStatefulComponent(instance: ComponentInternalInstance) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
handleSetupResult(instance, setupResult)
|
handleSetupResult(instance, setupResult, parentSuspense)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
finishComponentSetup(instance)
|
finishComponentSetup(instance, parentSuspense)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleSetupResult(
|
export function handleSetupResult(
|
||||||
instance: ComponentInternalInstance,
|
instance: ComponentInternalInstance,
|
||||||
setupResult: unknown
|
setupResult: unknown,
|
||||||
|
parentSuspense: SuspenseBoundary | null
|
||||||
) {
|
) {
|
||||||
if (isFunction(setupResult)) {
|
if (isFunction(setupResult)) {
|
||||||
// setup returned an inline render function
|
// 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
|
const Component = instance.type as ComponentOptions
|
||||||
if (!instance.render) {
|
if (!instance.render) {
|
||||||
if (__DEV__ && !Component.render) {
|
if (__DEV__ && !Component.render) {
|
||||||
@ -299,8 +310,10 @@ function finishComponentSetup(instance: ComponentInternalInstance) {
|
|||||||
// support for 2.x options
|
// support for 2.x options
|
||||||
if (__FEATURE_OPTIONS__) {
|
if (__FEATURE_OPTIONS__) {
|
||||||
currentInstance = instance
|
currentInstance = instance
|
||||||
|
currentSuspense = parentSuspense
|
||||||
applyOptions(instance, Component)
|
applyOptions(instance, Component)
|
||||||
currentInstance = null
|
currentInstance = null
|
||||||
|
currentSuspense = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.renderContext === EMPTY_OBJ) {
|
if (instance.renderContext === EMPTY_OBJ) {
|
||||||
|
@ -78,7 +78,7 @@ function invokeHooks(hooks: Function[], arg?: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queuePostEffect(
|
export function queuePostRenderEffect(
|
||||||
fn: Function | Function[],
|
fn: Function | Function[],
|
||||||
suspense: SuspenseBoundary<any, any> | null
|
suspense: SuspenseBoundary<any, any> | null
|
||||||
) {
|
) {
|
||||||
@ -357,7 +357,7 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
hostInsert(el, container, anchor)
|
hostInsert(el, container, anchor)
|
||||||
if (props != null && props.vnodeMounted != null) {
|
if (props != null && props.vnodeMounted != null) {
|
||||||
queuePostEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
invokeDirectiveHook(props.vnodeMounted, parentComponent, vnode)
|
invokeDirectiveHook(props.vnodeMounted, parentComponent, vnode)
|
||||||
}, parentSuspense)
|
}, parentSuspense)
|
||||||
}
|
}
|
||||||
@ -508,7 +508,7 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newProps.vnodeUpdated != null) {
|
if (newProps.vnodeUpdated != null) {
|
||||||
queuePostEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1)
|
invokeDirectiveHook(newProps.vnodeUpdated, parentComponent, n2, n1)
|
||||||
}, parentSuspense)
|
}, parentSuspense)
|
||||||
}
|
}
|
||||||
@ -700,7 +700,9 @@ export function createRenderer<
|
|||||||
function resolveSuspense() {
|
function resolveSuspense() {
|
||||||
const { subTree, fallbackTree, effects, vnode } = suspense
|
const { subTree, fallbackTree, effects, vnode } = suspense
|
||||||
// unmount fallback tree
|
// unmount fallback tree
|
||||||
|
if (fallback.el) {
|
||||||
unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
|
unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
|
||||||
|
}
|
||||||
// move content from off-dom container to actual container
|
// move content from off-dom container to actual container
|
||||||
move(subTree as HostVNode, container, anchor)
|
move(subTree as HostVNode, container, anchor)
|
||||||
const el = (vnode.el = (subTree as HostVNode).el as HostNode)
|
const el = (vnode.el = (subTree as HostVNode).el as HostNode)
|
||||||
@ -895,7 +897,7 @@ export function createRenderer<
|
|||||||
|
|
||||||
// setup stateful logic
|
// setup stateful logic
|
||||||
if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
||||||
setupStatefulComponent(instance)
|
setupStatefulComponent(instance, parentSuspense)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup() is async. This component relies on async logic to be resolved
|
// setup() is async. This component relies on async logic to be resolved
|
||||||
@ -909,7 +911,7 @@ export function createRenderer<
|
|||||||
parentSuspense.deps--
|
parentSuspense.deps--
|
||||||
// retry from this component
|
// retry from this component
|
||||||
instance.asyncResolved = true
|
instance.asyncResolved = true
|
||||||
handleSetupResult(instance, asyncSetupResult)
|
handleSetupResult(instance, asyncSetupResult, parentSuspense)
|
||||||
setupRenderEffect(
|
setupRenderEffect(
|
||||||
instance,
|
instance,
|
||||||
parentSuspense,
|
parentSuspense,
|
||||||
@ -965,7 +967,7 @@ export function createRenderer<
|
|||||||
initialVNode.el = subTree.el
|
initialVNode.el = subTree.el
|
||||||
// mounted hook
|
// mounted hook
|
||||||
if (instance.m !== null) {
|
if (instance.m !== null) {
|
||||||
queuePostEffect(instance.m, parentSuspense)
|
queuePostRenderEffect(instance.m, parentSuspense)
|
||||||
}
|
}
|
||||||
mounted = true
|
mounted = true
|
||||||
} else {
|
} else {
|
||||||
@ -1018,7 +1020,7 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
// upated hook
|
// upated hook
|
||||||
if (instance.u !== null) {
|
if (instance.u !== null) {
|
||||||
queuePostEffect(instance.u, parentSuspense)
|
queuePostRenderEffect(instance.u, parentSuspense)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
@ -1500,7 +1502,7 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (props != null && props.vnodeUnmounted != null) {
|
if (props != null && props.vnodeUnmounted != null) {
|
||||||
queuePostEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
invokeDirectiveHook(props.vnodeUnmounted, parentComponent, vnode)
|
invokeDirectiveHook(props.vnodeUnmounted, parentComponent, vnode)
|
||||||
}, parentSuspense)
|
}, parentSuspense)
|
||||||
}
|
}
|
||||||
@ -1525,9 +1527,9 @@ export function createRenderer<
|
|||||||
unmount(subTree, instance, parentSuspense, doRemove)
|
unmount(subTree, instance, parentSuspense, doRemove)
|
||||||
// unmounted hook
|
// unmounted hook
|
||||||
if (um !== null) {
|
if (um !== null) {
|
||||||
queuePostEffect(um, parentSuspense)
|
queuePostRenderEffect(um, parentSuspense)
|
||||||
// set unmounted after unmounted hooks are fired
|
// set unmounted after unmounted hooks are fired
|
||||||
queuePostEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
instance.isUnmounted = true
|
instance.isUnmounted = true
|
||||||
}, parentSuspense)
|
}, parentSuspense)
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ import { isFunction } from '@vue/shared'
|
|||||||
export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol()
|
export const SuspenseSymbol = __DEV__ ? Symbol('Suspense key') : Symbol()
|
||||||
|
|
||||||
export interface SuspenseBoundary<
|
export interface SuspenseBoundary<
|
||||||
HostNode,
|
HostNode = any,
|
||||||
HostElement,
|
HostElement = any,
|
||||||
HostVNode = VNode<HostNode, HostElement>
|
HostVNode = VNode<HostNode, HostElement>
|
||||||
> {
|
> {
|
||||||
vnode: HostVNode
|
vnode: HostVNode
|
||||||
|
Loading…
x
Reference in New Issue
Block a user