From 1c628d0b79ec31c8f44fec074a2728cf8b1e0bdc Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 12 Sep 2019 12:16:01 -0400 Subject: [PATCH] feat: re-suspense when encountering new async deps in resolved state --- .../__tests__/rendererSuspense.spec.ts | 44 ++++++++++++- packages/runtime-core/src/createRenderer.ts | 65 ++++++++++++++++--- packages/runtime-core/src/suspense.ts | 8 ++- 3 files changed, 107 insertions(+), 10 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts index e73a81c3..7068ab0a 100644 --- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts +++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts @@ -658,7 +658,49 @@ describe('renderer: suspense', () => { ) }) - test.todo('new async dep after resolve should cause suspense to restart') + test('new async dep after resolve should cause suspense to restart', async () => { + const toggle = ref(false) + + const ChildA = createAsyncComponent({ + setup() { + return () => h('div', 'Child A') + } + }) + + const ChildB = createAsyncComponent({ + setup() { + return () => h('div', 'Child B') + } + }) + + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: [h(ChildA), toggle.value ? h(ChildB) : null], + fallback: h('div', 'root fallback') + }) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
root fallback
`) + + await deps[0] + await nextTick() + expect(serializeInner(root)).toBe(`
Child A
`) + + toggle.value = true + await nextTick() + expect(serializeInner(root)).toBe(`
root fallback
`) + + await deps[1] + await nextTick() + expect(serializeInner(root)).toBe( + `
Child A
Child B
` + ) + }) test.todo('portal inside suspense') }) diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 73488df3..22cf5ef9 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -729,7 +729,9 @@ export function createRenderer< parentComponent, container, hiddenContainer, - anchor + anchor, + isSVG, + optimized )) const { content, fallback } = normalizeSuspenseChildren(n2) @@ -831,20 +833,20 @@ export function createRenderer< if (__DEV__) { if (suspense.isResolved) { throw new Error( - `suspense.resolve() is called when it's already resolved` + `resolveSuspense() is called on an already resolved suspense boundary.` ) } if (suspense.isUnmounted) { throw new Error( - `suspense.resolve() is called when it's already unmounted` + `resolveSuspense() is called on an already unmounted suspense boundary.` ) } } const { + vnode, subTree, fallbackTree, effects, - vnode, parentComponent, container } = suspense @@ -891,6 +893,47 @@ export function createRenderer< } } + function restartSuspense(suspense: HostSuspsenseBoundary) { + suspense.isResolved = false + const { + vnode, + subTree, + fallbackTree, + parentComponent, + container, + hiddenContainer, + isSVG, + optimized + } = suspense + + // move content tree back to the off-dom container + const anchor = getNextHostNode(subTree) + move(subTree as HostVNode, hiddenContainer, null) + // remount the fallback tree + patch( + null, + fallbackTree, + container, + anchor, + parentComponent, + null, // fallback tree will not have suspense context + isSVG, + optimized + ) + const el = (vnode.el = (fallbackTree as HostVNode).el as HostNode) + // suspense as the root node of a component... + if (parentComponent && parentComponent.subTree === vnode) { + parentComponent.vnode.el = el + updateHOCHostEl(parentComponent, el) + } + + // invoke @suspense event + const onSuspense = vnode.props && vnode.props.onSuspense + if (isFunction(onSuspense)) { + onSuspense() + } + } + function processComponent( n1: HostVNode | null, n2: HostVNode, @@ -986,10 +1029,16 @@ export function createRenderer< // TODO handle this properly throw new Error('Async component without a suspense boundary!') } + + // parent suspense already resolved, need to re-suspense + // use queueJob so it's handled synchronously after patching the current + // suspense tree if (parentSuspense.isResolved) { - // TODO if parentSuspense is already resolved it needs to enter waiting - // state again + queueJob(() => { + restartSuspense(parentSuspense) + }) } + parentSuspense.deps++ instance.asyncDep .catch(err => { @@ -1006,6 +1055,7 @@ export function createRenderer< ) } }) + // give it a placeholder const placeholder = (instance.subTree = createVNode(Empty)) processEmptyNode(null, placeholder, container, anchor) @@ -1118,8 +1168,7 @@ export function createRenderer< parentSuspense, isSVG ) - let current = instance.vnode - current.el = nextTree.el + instance.vnode.el = nextTree.el if (next === null) { // self-triggered update. In case of HOC, update parent component // vnode el. HOC is indicated by parent instance's subTree pointing diff --git a/packages/runtime-core/src/suspense.ts b/packages/runtime-core/src/suspense.ts index fb8c36fb..09a067ab 100644 --- a/packages/runtime-core/src/suspense.ts +++ b/packages/runtime-core/src/suspense.ts @@ -13,6 +13,8 @@ export interface SuspenseBoundary< vnode: HostVNode parent: SuspenseBoundary | null parentComponent: ComponentInternalInstance | null + isSVG: boolean + optimized: boolean container: HostElement hiddenContainer: HostElement anchor: HostNode | null @@ -30,12 +32,16 @@ export function createSuspenseBoundary( parentComponent: ComponentInternalInstance | null, container: HostElement, hiddenContainer: HostElement, - anchor: HostNode | null + anchor: HostNode | null, + isSVG: boolean, + optimized: boolean ): SuspenseBoundary { return { vnode, parent, parentComponent, + isSVG, + optimized, container, hiddenContainer, anchor,