diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts index fa5b6716..b81d3844 100644 --- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts +++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts @@ -9,7 +9,8 @@ import { nextTick, onMounted, watch, - onUnmounted + onUnmounted, + onErrorCaptured } from '@vue/runtime-test' describe('renderer: suspense', () => { @@ -543,7 +544,39 @@ describe('renderer: suspense', () => { expect(calls).toEqual([`inner mounted`, `outer mounted`]) }) - test.todo('error handling') + test('error handling', async () => { + const Async = { + async setup() { + throw new Error('oops') + } + } + + const Comp = { + setup() { + const error = ref(null) + onErrorCaptured(e => { + error.value = e + return true + }) + + return () => + error.value + ? h('div', error.value.message) + : h(Suspense, null, { + default: h(Async), + fallback: h('div', 'fallback') + }) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
fallback
`) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
oops
`) + }) test.todo('new async dep after resolve should cause suspense to restart') diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 4a391847..43040e7a 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -49,6 +49,7 @@ import { createSuspenseBoundary, normalizeSuspenseChildren } from './suspense' +import { handleError, ErrorCodes } from './errorHandling' const prodEffectOptions = { scheduler: queueJob @@ -919,7 +920,7 @@ export function createRenderer< if (__DEV__) { pushWarningContext(n2) } - updateComponentPropsAndSlots(instance, n2) + updateComponentPreRender(instance, n2) if (__DEV__) { popWarningContext() } @@ -985,28 +986,21 @@ export function createRenderer< // state again } parentSuspense.deps++ - instance.asyncDep.then(asyncSetupResult => { - // unmounted before resolve - if (instance.isUnmounted || parentSuspense.isUnmounted) { - return - } - parentSuspense.deps-- - // retry from this component - instance.asyncResolved = true - handleSetupResult(instance, asyncSetupResult, parentSuspense) - setupRenderEffect( - instance, - parentSuspense, - initialVNode, - container, - anchor, - isSVG - ) - updateHOCHostEl(instance, initialVNode.el as HostNode) - if (parentSuspense.deps === 0) { - resolveSuspense(parentSuspense) - } - }) + instance.asyncDep + .catch(err => { + handleError(err, instance, ErrorCodes.SETUP_FUNCTION) + }) + .then(asyncSetupResult => { + // component may be unmounted before resolve + if (!instance.isUnmounted && !parentSuspense.isUnmounted) { + retryAsyncComponent( + instance, + asyncSetupResult, + parentSuspense, + isSVG + ) + } + }) // give it a placeholder const placeholder = (instance.subTree = createVNode(Empty)) processEmptyNode(null, placeholder, container, anchor) @@ -1028,6 +1022,38 @@ export function createRenderer< } } + function retryAsyncComponent( + instance: ComponentInternalInstance, + asyncSetupResult: unknown, + parentSuspense: HostSuspsenseBoundary, + isSVG: boolean + ) { + parentSuspense.deps-- + // retry from this component + instance.asyncResolved = true + const { vnode } = instance + if (__DEV__) { + pushWarningContext(vnode) + } + handleSetupResult(instance, asyncSetupResult, parentSuspense) + setupRenderEffect( + instance, + parentSuspense, + vnode, + // component may have been moved before resolve + hostParentNode(instance.subTree.el) as HostElement, + getNextHostNode(instance.subTree), + isSVG + ) + updateHOCHostEl(instance, vnode.el as HostNode) + if (__DEV__) { + popWarningContext() + } + if (parentSuspense.deps === 0) { + resolveSuspense(parentSuspense) + } + } + function setupRenderEffect( instance: ComponentInternalInstance, parentSuspense: HostSuspsenseBoundary | null, @@ -1063,7 +1089,7 @@ export function createRenderer< } if (next !== null) { - updateComponentPropsAndSlots(instance, next) + updateComponentPreRender(instance, next) } const prevTree = instance.subTree const nextTree = (instance.subTree = renderComponentRoot(instance)) @@ -1107,7 +1133,7 @@ export function createRenderer< }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) } - function updateComponentPropsAndSlots( + function updateComponentPreRender( instance: ComponentInternalInstance, nextVNode: HostVNode ) { @@ -1679,10 +1705,21 @@ export function createRenderer< } } - function getNextHostNode(vnode: HostVNode): HostNode | null { - return vnode.component === null - ? hostNextSibling((vnode.anchor || vnode.el) as HostNode) - : getNextHostNode(vnode.component.subTree) + function getNextHostNode({ + component, + suspense, + anchor, + el + }: HostVNode): HostNode | null { + if (component !== null) { + return getNextHostNode(component.subTree) + } + if (__FEATURE_SUSPENSE__ && suspense !== null) { + return getNextHostNode( + suspense.isResolved ? suspense.subTree : suspense.fallbackTree + ) + } + return hostNextSibling((anchor || el) as HostNode) } function setRef( diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 7a614c85..678b0a3d 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -43,7 +43,7 @@ export const ErrorTypeStrings: Record = { [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler', [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler', [ErrorCodes.SCHEDULER]: - 'scheduler flush. This may be a Vue internals bug. ' + + 'scheduler flush. This is likely a Vue internals bug. ' + 'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue' }