wip: watcher callback handling inside suspense

This commit is contained in:
Evan You 2019-09-11 10:09:00 -04:00
parent 51914c76e8
commit 356a01780b
6 changed files with 56 additions and 26 deletions

View File

@ -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')

View File

@ -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.`
: ``)
) )
} }
} }

View File

@ -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
} }

View File

@ -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) {

View File

@ -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
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 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)
} }

View File

@ -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