diff --git a/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts b/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
index 5c3f6a6b85e..0ba6079ab3d 100644
--- a/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
+++ b/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts
@@ -406,7 +406,7 @@ describe('api: defineAsyncComponent', () => {
const app = createApp({
render: () =>
h(Suspense, null, {
- default: () => [h(Foo), ' & ', h(Foo)],
+ default: () => h('div', [h(Foo), ' & ', h(Foo)]),
fallback: () => 'loading'
})
})
@@ -416,7 +416,7 @@ describe('api: defineAsyncComponent', () => {
resolve!(() => 'resolved')
await timeout()
- expect(serializeInner(root)).toBe('resolved & resolved')
+ expect(serializeInner(root)).toBe('
resolved & resolved
')
})
test('suspensible: false', async () => {
@@ -433,18 +433,18 @@ describe('api: defineAsyncComponent', () => {
const app = createApp({
render: () =>
h(Suspense, null, {
- default: () => [h(Foo), ' & ', h(Foo)],
+ default: () => h('div', [h(Foo), ' & ', h(Foo)]),
fallback: () => 'loading'
})
})
app.mount(root)
// should not show suspense fallback
- expect(serializeInner(root)).toBe(' & ')
+ expect(serializeInner(root)).toBe(' &
')
resolve!(() => 'resolved')
await timeout()
- expect(serializeInner(root)).toBe('resolved & resolved')
+ expect(serializeInner(root)).toBe('resolved & resolved
')
})
test('suspense with error handling', async () => {
@@ -460,7 +460,7 @@ describe('api: defineAsyncComponent', () => {
const app = createApp({
render: () =>
h(Suspense, null, {
- default: () => [h(Foo), ' & ', h(Foo)],
+ default: () => h('div', [h(Foo), ' & ', h(Foo)]),
fallback: () => 'loading'
})
})
@@ -472,7 +472,7 @@ describe('api: defineAsyncComponent', () => {
reject!(new Error('no'))
await timeout()
expect(handler).toHaveBeenCalled()
- expect(serializeInner(root)).toBe(' & ')
+ expect(serializeInner(root)).toBe(' &
')
})
test('retry (success)', async () => {
diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts
index ffc9f903ffa..e4a57804314 100644
--- a/packages/runtime-core/__tests__/components/Suspense.spec.ts
+++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts
@@ -11,7 +11,8 @@ import {
watch,
watchEffect,
onUnmounted,
- onErrorCaptured
+ onErrorCaptured,
+ shallowRef
} from '@vue/runtime-test'
describe('Suspense', () => {
@@ -490,7 +491,7 @@ describe('Suspense', () => {
setup() {
return () =>
h(Suspense, null, {
- default: [h(AsyncOuter), h(Inner)],
+ default: h('div', [h(AsyncOuter), h(Inner)]),
fallback: h('div', 'fallback outer')
})
}
@@ -503,14 +504,14 @@ describe('Suspense', () => {
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(
- `async outer
fallback inner
`
+ `async outer
fallback inner
`
)
expect(calls).toEqual([`outer mounted`])
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
- `async outer
async inner
`
+ ``
)
expect(calls).toEqual([`outer mounted`, `inner mounted`])
})
@@ -556,7 +557,7 @@ describe('Suspense', () => {
setup() {
return () =>
h(Suspense, null, {
- default: [h(AsyncOuter), h(Inner)],
+ default: h('div', [h(AsyncOuter), h(Inner)]),
fallback: h('div', 'fallback outer')
})
}
@@ -574,7 +575,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
- `async outer
async inner
`
+ ``
)
expect(calls).toEqual([`inner mounted`, `outer mounted`])
})
@@ -683,12 +684,12 @@ describe('Suspense', () => {
setup() {
return () =>
h(Suspense, null, {
- default: [
+ default: h('div', [
h(MiddleComponent),
h(AsyncChildParent, {
msg: 'root async'
})
- ],
+ ]),
fallback: h('div', 'root fallback')
})
}
@@ -722,7 +723,7 @@ describe('Suspense', () => {
await deps[3]
await nextTick()
expect(serializeInner(root)).toBe(
- `nested fallback
root async
`
+ `nested fallback
root async
`
)
expect(calls).toEqual([0, 1, 3])
@@ -733,7 +734,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
- `nested changed
root async
`
+ ``
)
expect(calls).toEqual([0, 1, 3, 2])
@@ -741,51 +742,316 @@ describe('Suspense', () => {
msg.value = 'nested changed again'
await nextTick()
expect(serializeInner(root)).toBe(
- `nested changed again
root async
`
+ `nested changed again
root async
`
)
})
- test('new async dep after resolve should cause suspense to restart', async () => {
- const toggle = ref(false)
+ test('switching branches', async () => {
+ const calls: string[] = []
+ const toggle = ref(true)
- const ChildA = defineAsyncComponent({
+ const Foo = defineAsyncComponent({
setup() {
- return () => h('div', 'Child A')
+ onMounted(() => {
+ calls.push('foo mounted')
+ })
+ onUnmounted(() => {
+ calls.push('foo unmounted')
+ })
+ return () => h('div', ['foo', h(FooNested)])
}
})
- const ChildB = defineAsyncComponent({
+ const FooNested = defineAsyncComponent(
+ {
+ setup() {
+ onMounted(() => {
+ calls.push('foo nested mounted')
+ })
+ onUnmounted(() => {
+ calls.push('foo nested unmounted')
+ })
+ return () => h('div', 'foo nested')
+ }
+ },
+ 10
+ )
+
+ const Bar = defineAsyncComponent(
+ {
+ setup() {
+ onMounted(() => {
+ calls.push('bar mounted')
+ })
+ onUnmounted(() => {
+ calls.push('bar unmounted')
+ })
+ return () => h('div', 'bar')
+ }
+ },
+ 10
+ )
+
+ const Comp = {
setup() {
- return () => h('div', 'Child B')
+ return () =>
+ h(Suspense, null, {
+ default: toggle.value ? h(Foo) : h(Bar),
+ fallback: h('div', 'fallback')
+ })
}
- })
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback
`)
+ expect(calls).toEqual([])
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`fallback
`)
+ expect(calls).toEqual([])
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(calls).toEqual([`foo mounted`, `foo nested mounted`])
+ expect(serializeInner(root)).toBe(``)
+
+ // toggle
+ toggle.value = false
+ await nextTick()
+ expect(deps.length).toBe(3)
+ // should remain on current view
+ expect(calls).toEqual([`foo mounted`, `foo nested mounted`])
+ expect(serializeInner(root)).toBe(``)
+
+ await Promise.all(deps)
+ await nextTick()
+ const tempCalls = [
+ `foo mounted`,
+ `foo nested mounted`,
+ `bar mounted`,
+ `foo nested unmounted`,
+ `foo unmounted`
+ ]
+ expect(calls).toEqual(tempCalls)
+ expect(serializeInner(root)).toBe(`bar
`)
+
+ // toggle back
+ toggle.value = true
+ await nextTick()
+ // should remain
+ expect(calls).toEqual(tempCalls)
+ expect(serializeInner(root)).toBe(`bar
`)
+
+ await deps[3]
+ await nextTick()
+ // still pending...
+ expect(calls).toEqual(tempCalls)
+ expect(serializeInner(root)).toBe(`bar
`)
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(calls).toEqual([
+ ...tempCalls,
+ `foo mounted`,
+ `foo nested mounted`,
+ `bar unmounted`
+ ])
+ expect(serializeInner(root)).toBe(``)
+ })
+
+ test('branch switch to 3rd branch before resolve', async () => {
+ const calls: string[] = []
+
+ const makeComp = (name: string, delay = 0) =>
+ defineAsyncComponent(
+ {
+ setup() {
+ onMounted(() => {
+ calls.push(`${name} mounted`)
+ })
+ onUnmounted(() => {
+ calls.push(`${name} unmounted`)
+ })
+ return () => h('div', [name])
+ }
+ },
+ delay
+ )
+
+ const One = makeComp('one')
+ const Two = makeComp('two', 10)
+ const Three = makeComp('three', 20)
+
+ const view = shallowRef(One)
const Comp = {
setup() {
return () =>
h(Suspense, null, {
- default: [h(ChildA), toggle.value ? h(ChildB) : null],
- fallback: h('div', 'root fallback')
+ default: h(view.value),
+ fallback: h('div', 'fallback')
})
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
- expect(serializeInner(root)).toBe(`root fallback
`)
+ expect(serializeInner(root)).toBe(`fallback
`)
+ expect(calls).toEqual([])
await deps[0]
await nextTick()
- expect(serializeInner(root)).toBe(`Child A
`)
+ expect(serializeInner(root)).toBe(`one
`)
+ expect(calls).toEqual([`one mounted`])
- toggle.value = true
+ view.value = Two
await nextTick()
- expect(serializeInner(root)).toBe(`root fallback
`)
+ expect(deps.length).toBe(2)
+
+ // switch before two resovles
+ view.value = Three
+ await nextTick()
+ expect(deps.length).toBe(3)
+ // dep for two resolves
await deps[1]
await nextTick()
- expect(serializeInner(root)).toBe(`Child A
Child B
`)
+ // should still be on view one
+ expect(serializeInner(root)).toBe(`one
`)
+ expect(calls).toEqual([`one mounted`])
+
+ await deps[2]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`three
`)
+ expect(calls).toEqual([`one mounted`, `three mounted`, `one unmounted`])
})
- test.todo('teleport inside suspense')
+ test('branch switch back before resolve', async () => {
+ const calls: string[] = []
+
+ const makeComp = (name: string, delay = 0) =>
+ defineAsyncComponent(
+ {
+ setup() {
+ onMounted(() => {
+ calls.push(`${name} mounted`)
+ })
+ onUnmounted(() => {
+ calls.push(`${name} unmounted`)
+ })
+ return () => h('div', [name])
+ }
+ },
+ delay
+ )
+
+ const One = makeComp('one')
+ const Two = makeComp('two', 10)
+
+ const view = shallowRef(One)
+
+ const Comp = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: h(view.value),
+ fallback: h('div', 'fallback')
+ })
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback
`)
+ expect(calls).toEqual([])
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`one
`)
+ expect(calls).toEqual([`one mounted`])
+
+ view.value = Two
+ await nextTick()
+ expect(deps.length).toBe(2)
+
+ // switch back before two resovles
+ view.value = One
+ await nextTick()
+ expect(deps.length).toBe(2)
+
+ // dep for two resolves
+ await deps[1]
+ await nextTick()
+ // should still be on view one
+ expect(serializeInner(root)).toBe(`one
`)
+ expect(calls).toEqual([`one mounted`])
+ })
+
+ test('branch switch timeout + fallback', async () => {
+ const calls: string[] = []
+
+ const makeComp = (name: string, delay = 0) =>
+ defineAsyncComponent(
+ {
+ setup() {
+ onMounted(() => {
+ calls.push(`${name} mounted`)
+ })
+ onUnmounted(() => {
+ calls.push(`${name} unmounted`)
+ })
+ return () => h('div', [name])
+ }
+ },
+ delay
+ )
+
+ const One = makeComp('one')
+ const Two = makeComp('two', 20)
+
+ const view = shallowRef(One)
+
+ const Comp = {
+ setup() {
+ return () =>
+ h(
+ Suspense,
+ {
+ timeout: 10
+ },
+ {
+ default: h(view.value),
+ fallback: h('div', 'fallback')
+ }
+ )
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback
`)
+ expect(calls).toEqual([])
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`one
`)
+ expect(calls).toEqual([`one mounted`])
+
+ view.value = Two
+ await nextTick()
+ expect(serializeInner(root)).toBe(`one
`)
+ expect(calls).toEqual([`one mounted`])
+
+ await new Promise(r => setTimeout(r, 10))
+ await nextTick()
+ expect(serializeInner(root)).toBe(`fallback
`)
+ expect(calls).toEqual([`one mounted`, `one unmounted`])
+
+ await deps[1]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`two
`)
+ expect(calls).toEqual([`one mounted`, `one unmounted`, `two mounted`])
+ })
})
diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index fa89631d201..60978622bc6 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -506,8 +506,10 @@ describe('SSR hydration', () => {
const App = {
template: `
-
-
+
`,
components: {
AsyncChild
@@ -521,7 +523,7 @@ describe('SSR hydration', () => {
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot(
- `"1 2 "`
+ `"1 2
"`
)
// reset asyncDeps from ssr
asyncDeps.length = 0
@@ -537,17 +539,23 @@ describe('SSR hydration', () => {
// should flush buffered effects
expect(mountedCalls).toMatchObject([1, 2])
- expect(container.innerHTML).toMatch(`1 2 `)
+ expect(container.innerHTML).toMatch(
+ `1 2
`
+ )
const span1 = container.querySelector('span')!
triggerEvent('click', span1)
await nextTick()
- expect(container.innerHTML).toMatch(`2 2 `)
+ expect(container.innerHTML).toMatch(
+ `2 2
`
+ )
const span2 = span1.nextSibling as Element
triggerEvent('click', span2)
await nextTick()
- expect(container.innerHTML).toMatch(`2 3 `)
+ expect(container.innerHTML).toMatch(
+ `2 3
`
+ )
})
test('async component', async () => {
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 5a74541eb74..ac482d6707a 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -317,6 +317,11 @@ export interface ComponentInternalInstance {
* @internal
*/
suspense: SuspenseBoundary | null
+ /**
+ * suspense pending batch id
+ * @internal
+ */
+ suspenseId: number
/**
* @internal
*/
@@ -440,6 +445,7 @@ export function createComponentInstance(
// suspense related
suspense,
+ suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null,
asyncResolved: false,
diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts
index 3ca38498d9b..fc0ebb9e42c 100644
--- a/packages/runtime-core/src/components/BaseTransition.ts
+++ b/packages/runtime-core/src/components/BaseTransition.ts
@@ -52,10 +52,13 @@ export interface BaseTransitionProps {
export interface TransitionHooks<
HostElement extends RendererElement = RendererElement
> {
+ mode: BaseTransitionProps['mode']
persisted: boolean
beforeEnter(el: HostElement): void
enter(el: HostElement): void
leave(el: HostElement, remove: () => void): void
+ clone(vnode: VNode): TransitionHooks
+ // optional
afterLeave?(): void
delayLeave?(
el: HostElement,
@@ -174,12 +177,13 @@ const BaseTransitionImpl = {
return emptyPlaceholder(child)
}
- const enterHooks = (innerChild.transition = resolveTransitionHooks(
+ const enterHooks = resolveTransitionHooks(
innerChild,
rawProps,
state,
instance
- ))
+ )
+ setTransitionHooks(innerChild, enterHooks)
const oldChild = instance.subTree
const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
@@ -271,8 +275,13 @@ function getLeavingNodesForType(
// and will be called at appropriate timing in the renderer.
export function resolveTransitionHooks(
vnode: VNode,
- {
+ props: BaseTransitionProps,
+ state: TransitionState,
+ instance: ComponentInternalInstance
+): TransitionHooks {
+ const {
appear,
+ mode,
persisted = false,
onBeforeEnter,
onEnter,
@@ -286,10 +295,7 @@ export function resolveTransitionHooks(
onAppear,
onAfterAppear,
onAppearCancelled
- }: BaseTransitionProps,
- state: TransitionState,
- instance: ComponentInternalInstance
-): TransitionHooks {
+ } = props
const key = String(vnode.key)
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
@@ -304,6 +310,7 @@ export function resolveTransitionHooks(
}
const hooks: TransitionHooks = {
+ mode,
persisted,
beforeEnter(el) {
let hook = onBeforeEnter
@@ -401,6 +408,10 @@ export function resolveTransitionHooks(
} else {
done()
}
+ },
+
+ clone(vnode) {
+ return resolveTransitionHooks(vnode, props, state, instance)
}
}
@@ -430,6 +441,9 @@ function getKeepAliveChild(vnode: VNode): VNode | undefined {
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
setTransitionHooks(vnode.component.subTree, hooks)
+ } else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
+ vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
+ vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
} else {
vnode.transition = hooks
}
diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts
index 11fdd0fc621..3259ca685c6 100644
--- a/packages/runtime-core/src/components/KeepAlive.ts
+++ b/packages/runtime-core/src/components/KeepAlive.ts
@@ -184,7 +184,7 @@ const KeepAliveImpl = {
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
- cache.set(pendingCacheKey, instance.subTree)
+ cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
@@ -193,11 +193,12 @@ const KeepAliveImpl = {
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
- if (cached.type === subTree.type) {
+ const vnode = getInnerChild(subTree)
+ if (cached.type === vnode.type) {
// current instance will be unmounted as part of keep-alive's unmount
- resetShapeFlag(subTree)
+ resetShapeFlag(vnode)
// but invoke its deactivated hook here
- const da = subTree.component!.da
+ const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
@@ -213,7 +214,7 @@ const KeepAliveImpl = {
}
const children = slots.default()
- let vnode = children[0]
+ const rawVNode = children[0]
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
@@ -221,13 +222,15 @@ const KeepAliveImpl = {
current = null
return children
} else if (
- !isVNode(vnode) ||
- !(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)
+ !isVNode(rawVNode) ||
+ (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
+ !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
- return vnode
+ return rawVNode
}
+ let vnode = getInnerChild(rawVNode)
const comp = vnode.type as ConcreteComponent
const name = getName(comp)
const { include, exclude, max } = props
@@ -236,7 +239,8 @@ const KeepAliveImpl = {
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
- return (current = vnode)
+ current = vnode
+ return rawVNode
}
const key = vnode.key == null ? comp : vnode.key
@@ -245,6 +249,9 @@ const KeepAliveImpl = {
// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {
vnode = cloneVNode(vnode)
+ if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
+ rawVNode.ssContent = vnode
+ }
}
// #1513 it's possible for the returned vnode to be cloned due to attr
// fallthrough or scopeId, so the vnode here may not be the final vnode
@@ -277,7 +284,7 @@ const KeepAliveImpl = {
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
- return vnode
+ return rawVNode
}
}
}
@@ -383,3 +390,7 @@ function resetShapeFlag(vnode: VNode) {
}
vnode.shapeFlag = shapeFlag
}
+
+function getInnerChild(vnode: VNode) {
+ return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode
+}
diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts
index 89ed331b0b7..129e62c2f8f 100644
--- a/packages/runtime-core/src/components/Suspense.ts
+++ b/packages/runtime-core/src/components/Suspense.ts
@@ -1,5 +1,11 @@
-import { VNode, normalizeVNode, VNodeChild, VNodeProps } from '../vnode'
-import { isFunction, isArray, ShapeFlags } from '@vue/shared'
+import {
+ VNode,
+ normalizeVNode,
+ VNodeChild,
+ VNodeProps,
+ isSameVNodeType
+} from '../vnode'
+import { isFunction, isArray, ShapeFlags, toNumber } from '@vue/shared'
import { ComponentInternalInstance, handleSetupResult } from '../component'
import { Slots } from '../componentSlots'
import {
@@ -9,14 +15,16 @@ import {
RendererNode,
RendererElement
} from '../renderer'
-import { queuePostFlushCb, queueJob } from '../scheduler'
-import { updateHOCHostEl } from '../componentRenderUtils'
-import { pushWarningContext, popWarningContext } from '../warning'
+import { queuePostFlushCb } from '../scheduler'
+import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
+import { pushWarningContext, popWarningContext, warn } from '../warning'
import { handleError, ErrorCodes } from '../errorHandling'
export interface SuspenseProps {
onResolve?: () => void
- onRecede?: () => void
+ onPending?: () => void
+ onFallback?: () => void
+ timeout?: string | number
}
export const isSuspense = (type: any): boolean => type.__isSuspense
@@ -66,7 +74,8 @@ export const SuspenseImpl = {
)
}
},
- hydrate: hydrateSuspense
+ hydrate: hydrateSuspense,
+ create: createSuspenseBoundary
}
// Force-casted public typing for h and TSX props inference
@@ -78,7 +87,7 @@ export const Suspense = ((__FEATURE_SUSPENSE__
}
function mountSuspense(
- n2: VNode,
+ vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
@@ -92,8 +101,8 @@ function mountSuspense(
o: { createElement }
} = rendererInternals
const hiddenContainer = createElement('div')
- const suspense = (n2.suspense = createSuspenseBoundary(
- n2,
+ const suspense = (vnode.suspense = createSuspenseBoundary(
+ vnode,
parentSuspense,
parentComponent,
container,
@@ -107,7 +116,7 @@ function mountSuspense(
// start mounting the content subtree in an off-dom container
patch(
null,
- suspense.subTree,
+ (suspense.pendingBranch = vnode.ssContent!),
hiddenContainer,
null,
parentComponent,
@@ -117,10 +126,11 @@ function mountSuspense(
)
// now check if we have encountered any async deps
if (suspense.deps > 0) {
+ // has async
// mount the fallback tree
patch(
null,
- suspense.fallbackTree,
+ vnode.ssFallback!,
container,
anchor,
parentComponent,
@@ -128,7 +138,7 @@ function mountSuspense(
isSVG,
optimized
)
- n2.el = suspense.fallbackTree.el
+ setActiveBranch(suspense, vnode.ssFallback!)
} else {
// Suspense has no async deps. Just resolve.
suspense.resolve()
@@ -143,57 +153,172 @@ function patchSuspense(
parentComponent: ComponentInternalInstance | null,
isSVG: boolean,
optimized: boolean,
- { p: patch }: RendererInternals
+ { p: patch, um: unmount, o: { createElement } }: RendererInternals
) {
const suspense = (n2.suspense = n1.suspense)!
suspense.vnode = n2
- const { content, fallback } = normalizeSuspenseChildren(n2)
- const oldSubTree = suspense.subTree
- const oldFallbackTree = suspense.fallbackTree
- if (!suspense.isResolved) {
- patch(
- oldSubTree,
- content,
- suspense.hiddenContainer,
- null,
- parentComponent,
- suspense,
- isSVG,
- optimized
- )
- if (suspense.deps > 0) {
- // still pending. patch the fallback tree.
+ n2.el = n1.el
+ const newBranch = n2.ssContent!
+ const newFallback = n2.ssFallback!
+
+ const { activeBranch, pendingBranch, isInFallback, isHydrating } = suspense
+ if (pendingBranch) {
+ suspense.pendingBranch = newBranch
+ if (isSameVNodeType(newBranch, pendingBranch)) {
+ // same root type but content may have changed.
patch(
- oldFallbackTree,
- fallback,
+ pendingBranch,
+ newBranch,
+ suspense.hiddenContainer,
+ null,
+ parentComponent,
+ suspense,
+ isSVG,
+ optimized
+ )
+ if (suspense.deps <= 0) {
+ suspense.resolve()
+ } else if (isInFallback) {
+ patch(
+ activeBranch,
+ newFallback,
+ container,
+ anchor,
+ parentComponent,
+ null, // fallback tree will not have suspense context
+ isSVG,
+ optimized
+ )
+ setActiveBranch(suspense, newFallback)
+ }
+ } else {
+ // toggled before pending tree is resolved
+ suspense.pendingId++
+ if (isHydrating) {
+ // if toggled before hydration is finished, the current DOM tree is
+ // no longer valid. set it as the active branch so it will be unmounted
+ // when resolved
+ suspense.isHydrating = false
+ suspense.activeBranch = pendingBranch
+ } else {
+ unmount(pendingBranch, parentComponent, null)
+ }
+ // increment pending ID. this is used to invalidate async callbacks
+ // reset suspense state
+ suspense.deps = 0
+ suspense.effects.length = 0
+ // discard previous container
+ suspense.hiddenContainer = createElement('div')
+
+ if (isInFallback) {
+ // already in fallback state
+ patch(
+ null,
+ newBranch,
+ suspense.hiddenContainer,
+ null,
+ parentComponent,
+ suspense,
+ isSVG,
+ optimized
+ )
+ if (suspense.deps <= 0) {
+ suspense.resolve()
+ } else {
+ patch(
+ activeBranch,
+ newFallback,
+ container,
+ anchor,
+ parentComponent,
+ null, // fallback tree will not have suspense context
+ isSVG,
+ optimized
+ )
+ setActiveBranch(suspense, newFallback)
+ }
+ } else if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
+ // toggled "back" to current active branch
+ patch(
+ activeBranch,
+ newBranch,
+ container,
+ anchor,
+ parentComponent,
+ suspense,
+ isSVG,
+ optimized
+ )
+ // force resolve
+ suspense.resolve(true)
+ } else {
+ // switched to a 3rd branch
+ patch(
+ null,
+ newBranch,
+ suspense.hiddenContainer,
+ null,
+ parentComponent,
+ suspense,
+ isSVG,
+ optimized
+ )
+ if (suspense.deps <= 0) {
+ suspense.resolve()
+ }
+ }
+ }
+ } else {
+ if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
+ // root did not change, just normal patch
+ patch(
+ activeBranch,
+ newBranch,
container,
anchor,
parentComponent,
- null, // fallback tree will not have suspense context
+ suspense,
isSVG,
optimized
)
- n2.el = fallback.el
+ setActiveBranch(suspense, newBranch)
+ } else {
+ // root node toggled
+ // invoke @pending event
+ const onPending = n2.props && n2.props.onPending
+ if (isFunction(onPending)) {
+ onPending()
+ }
+ // mount pending branch in off-dom container
+ suspense.pendingBranch = newBranch
+ suspense.pendingId++
+ patch(
+ null,
+ newBranch,
+ suspense.hiddenContainer,
+ null,
+ parentComponent,
+ suspense,
+ isSVG,
+ optimized
+ )
+ if (suspense.deps <= 0) {
+ // incoming branch has no async deps, resolve now.
+ suspense.resolve()
+ } else {
+ const { timeout, pendingId } = suspense
+ if (timeout > 0) {
+ setTimeout(() => {
+ if (suspense.pendingId === pendingId) {
+ suspense.fallback(newFallback)
+ }
+ }, timeout)
+ } else if (timeout === 0) {
+ suspense.fallback(newFallback)
+ }
+ }
}
- // If deps somehow becomes 0 after the patch it means the patch caused an
- // async dep component to unmount and removed its dep. It will cause the
- // suspense to resolve and we don't need to do anything here.
- } else {
- // just normal patch inner content as a fragment
- patch(
- oldSubTree,
- content,
- container,
- anchor,
- parentComponent,
- suspense,
- isSVG,
- optimized
- )
- n2.el = content.el
}
- suspense.subTree = content
- suspense.fallbackTree = fallback
}
export interface SuspenseBoundary {
@@ -205,15 +330,17 @@ export interface SuspenseBoundary {
container: RendererElement
hiddenContainer: RendererElement
anchor: RendererNode | null
- subTree: VNode
- fallbackTree: VNode
+ activeBranch: VNode | null
+ pendingBranch: VNode | null
deps: number
+ pendingId: number
+ timeout: number
+ isInFallback: boolean
isHydrating: boolean
- isResolved: boolean
isUnmounted: boolean
effects: Function[]
- resolve(): void
- recede(): void
+ resolve(force?: boolean): void
+ fallback(fallbackVNode: VNode): void
move(
container: RendererElement,
anchor: RendererNode | null,
@@ -255,15 +382,10 @@ function createSuspenseBoundary(
m: move,
um: unmount,
n: next,
- o: { parentNode }
+ o: { parentNode, remove }
} = rendererInternals
- const getCurrentTree = () =>
- suspense.isResolved || suspense.isHydrating
- ? suspense.subTree
- : suspense.fallbackTree
-
- const { content, fallback } = normalizeSuspenseChildren(vnode)
+ const timeout = toNumber(vnode.props && vnode.props.timeout)
const suspense: SuspenseBoundary = {
vnode,
parent,
@@ -274,30 +396,33 @@ function createSuspenseBoundary(
hiddenContainer,
anchor,
deps: 0,
- subTree: content,
- fallbackTree: fallback,
+ pendingId: 0,
+ timeout: typeof timeout === 'number' ? timeout : -1,
+ activeBranch: null,
+ pendingBranch: null,
+ isInFallback: true,
isHydrating,
- isResolved: false,
isUnmounted: false,
effects: [],
- resolve() {
+ resolve(resume = false) {
if (__DEV__) {
- if (suspense.isResolved) {
+ if (!resume && !suspense.pendingBranch) {
throw new Error(
- `resolveSuspense() is called on an already resolved suspense boundary.`
+ `suspense.resolve() is called without a pending branch.`
)
}
if (suspense.isUnmounted) {
throw new Error(
- `resolveSuspense() is called on an already unmounted suspense boundary.`
+ `suspense.resolve() is called on an already unmounted suspense boundary.`
)
}
}
const {
vnode,
- subTree,
- fallbackTree,
+ activeBranch,
+ pendingBranch,
+ pendingId,
effects,
parentComponent,
container
@@ -305,31 +430,43 @@ function createSuspenseBoundary(
if (suspense.isHydrating) {
suspense.isHydrating = false
- } else {
+ } else if (!resume) {
+ const delayEnter =
+ activeBranch &&
+ pendingBranch!.transition &&
+ pendingBranch!.transition.mode === 'out-in'
+ if (delayEnter) {
+ activeBranch!.transition!.afterLeave = () => {
+ if (pendingId === suspense.pendingId) {
+ move(pendingBranch!, container, anchor, MoveType.ENTER)
+ }
+ }
+ }
// this is initial anchor on mount
let { anchor } = suspense
- // unmount fallback tree
- if (fallbackTree.el) {
+ // unmount current active tree
+ if (activeBranch) {
// if the fallback tree was mounted, it may have been moved
// as part of a parent suspense. get the latest anchor for insertion
- anchor = next(fallbackTree)
- unmount(fallbackTree, parentComponent, suspense, true)
+ anchor = next(activeBranch)
+ unmount(activeBranch, parentComponent, suspense, true)
+ }
+ if (!delayEnter) {
+ // move content from off-dom container to actual container
+ move(pendingBranch!, container, anchor, MoveType.ENTER)
}
- // move content from off-dom container to actual container
- move(subTree, container, anchor, MoveType.ENTER)
}
- const el = (vnode.el = subTree.el!)
- // suspense as the root node of a component...
- if (parentComponent && parentComponent.subTree === vnode) {
- parentComponent.vnode.el = el
- updateHOCHostEl(parentComponent, el)
- }
+ setActiveBranch(suspense, pendingBranch!)
+ suspense.pendingBranch = null
+ suspense.isInFallback = false
+
+ // flush buffered effects
// check if there is a pending parent suspense
let parent = suspense.parent
let hasUnresolvedAncestor = false
while (parent) {
- if (!parent.isResolved) {
+ if (parent.pendingBranch) {
// found a pending parent suspense, merge buffered post jobs
// into that parent
parent.effects.push(...effects)
@@ -342,8 +479,8 @@ function createSuspenseBoundary(
if (!hasUnresolvedAncestor) {
queuePostFlushCb(effects)
}
- suspense.isResolved = true
suspense.effects = []
+
// invoke @resolve event
const onResolve = vnode.props && vnode.props.onResolve
if (isFunction(onResolve)) {
@@ -351,64 +488,77 @@ function createSuspenseBoundary(
}
},
- recede() {
- suspense.isResolved = false
+ fallback(fallbackVNode) {
+ if (!suspense.pendingBranch) {
+ return
+ }
+
const {
vnode,
- subTree,
- fallbackTree,
+ activeBranch,
parentComponent,
container,
- hiddenContainer,
isSVG,
optimized
} = suspense
- // move content tree back to the off-dom container
- const anchor = next(subTree)
- move(subTree, hiddenContainer, null, MoveType.LEAVE)
- // remount the fallback tree
- patch(
- null,
- fallbackTree,
- container,
- anchor,
+ // invoke @recede event
+ const onFallback = vnode.props && vnode.props.onFallback
+ if (isFunction(onFallback)) {
+ onFallback()
+ }
+
+ const anchor = next(activeBranch!)
+ const mountFallback = () => {
+ if (!suspense.isInFallback) {
+ return
+ }
+ // mount the fallback tree
+ patch(
+ null,
+ fallbackVNode,
+ container,
+ anchor,
+ parentComponent,
+ null, // fallback tree will not have suspense context
+ isSVG,
+ optimized
+ )
+ setActiveBranch(suspense, fallbackVNode)
+ }
+
+ const delayEnter =
+ fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
+ if (delayEnter) {
+ activeBranch!.transition!.afterLeave = mountFallback
+ }
+ // unmount current active branch
+ unmount(
+ activeBranch!,
parentComponent,
- null, // fallback tree will not have suspense context
- isSVG,
- optimized
+ null, // no suspense so unmount hooks fire now
+ true // shouldRemove
)
- const el = (vnode.el = fallbackTree.el!)
- // suspense as the root node of a component...
- if (parentComponent && parentComponent.subTree === vnode) {
- parentComponent.vnode.el = el
- updateHOCHostEl(parentComponent, el)
- }
- // invoke @recede event
- const onRecede = vnode.props && vnode.props.onRecede
- if (isFunction(onRecede)) {
- onRecede()
+ suspense.isInFallback = true
+ if (!delayEnter) {
+ mountFallback()
}
},
move(container, anchor, type) {
- move(getCurrentTree(), container, anchor, type)
+ suspense.activeBranch &&
+ move(suspense.activeBranch, container, anchor, type)
suspense.container = container
},
next() {
- return next(getCurrentTree())
+ return suspense.activeBranch && next(suspense.activeBranch)
},
registerDep(instance, setupRenderEffect) {
- // suspense is already resolved, need to recede.
- // use queueJob so it's handled synchronously after patching the current
- // suspense tree
- if (suspense.isResolved) {
- queueJob(() => {
- suspense.recede()
- })
+ if (!suspense.pendingBranch) {
+ return
}
const hydratedEl = instance.vnode.el
@@ -420,7 +570,11 @@ function createSuspenseBoundary(
.then(asyncSetupResult => {
// retry when the setup() promise resolves.
// component may have been unmounted before resolve.
- if (instance.isUnmounted || suspense.isUnmounted) {
+ if (
+ instance.isUnmounted ||
+ suspense.isUnmounted ||
+ suspense.pendingId !== instance.suspenseId
+ ) {
return
}
suspense.deps--
@@ -436,15 +590,14 @@ function createSuspenseBoundary(
// async dep is resolved.
vnode.el = hydratedEl
}
+ const placeholder = !hydratedEl && instance.subTree.el
setupRenderEffect(
instance,
vnode,
// component may have been moved before resolve.
// if this is not a hydration, instance.subTree will be the comment
// placeholder.
- hydratedEl
- ? parentNode(hydratedEl)!
- : parentNode(instance.subTree.el!)!,
+ parentNode(hydratedEl || instance.subTree.el!)!,
// anchor will not be used if this is hydration, so only need to
// consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree),
@@ -452,6 +605,9 @@ function createSuspenseBoundary(
isSVG,
optimized
)
+ if (placeholder) {
+ remove(placeholder)
+ }
updateHOCHostEl(instance, vnode.el)
if (__DEV__) {
popWarningContext()
@@ -464,10 +620,17 @@ function createSuspenseBoundary(
unmount(parentSuspense, doRemove) {
suspense.isUnmounted = true
- unmount(suspense.subTree, parentComponent, parentSuspense, doRemove)
- if (!suspense.isResolved) {
+ if (suspense.activeBranch) {
+ unmount(
+ suspense.activeBranch,
+ parentComponent,
+ parentSuspense,
+ doRemove
+ )
+ }
+ if (suspense.pendingBranch) {
unmount(
- suspense.fallbackTree,
+ suspense.pendingBranch,
parentComponent,
parentSuspense,
doRemove
@@ -516,7 +679,7 @@ function hydrateSuspense(
// need to construct a suspense boundary first
const result = hydrateNode(
node,
- suspense.subTree,
+ (suspense.pendingBranch = vnode.ssContent!),
parentComponent,
suspense,
optimized
@@ -535,25 +698,40 @@ export function normalizeSuspenseChildren(
fallback: VNode
} {
const { shapeFlag, children } = vnode
+ let content: VNode
+ let fallback: VNode
if (shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
- const { default: d, fallback } = children as Slots
- return {
- content: normalizeVNode(isFunction(d) ? d() : d),
- fallback: normalizeVNode(isFunction(fallback) ? fallback() : fallback)
- }
+ content = normalizeSuspenseSlot((children as Slots).default)
+ fallback = normalizeSuspenseSlot((children as Slots).fallback)
} else {
- return {
- content: normalizeVNode(children as VNodeChild),
- fallback: normalizeVNode(null)
+ content = normalizeSuspenseSlot(children as VNodeChild)
+ fallback = normalizeVNode(null)
+ }
+ return {
+ content,
+ fallback
+ }
+}
+
+function normalizeSuspenseSlot(s: any) {
+ if (isFunction(s)) {
+ s = s()
+ }
+ if (isArray(s)) {
+ const singleChild = filterSingleRoot(s)
+ if (__DEV__ && !singleChild) {
+ warn(` slots expect a single root node.`)
}
+ s = singleChild
}
+ return normalizeVNode(s)
}
export function queueEffectWithSuspense(
fn: Function | Function[],
suspense: SuspenseBoundary | null
): void {
- if (suspense && !suspense.isResolved) {
+ if (suspense && suspense.pendingBranch) {
if (isArray(fn)) {
suspense.effects.push(...fn)
} else {
@@ -563,3 +741,15 @@ export function queueEffectWithSuspense(
queuePostFlushCb(fn)
}
}
+
+function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
+ suspense.activeBranch = branch
+ const { vnode, parentComponent } = suspense
+ const el = (vnode.el = branch.el)
+ // in case suspense is the root node of a component,
+ // recursively update the HOC el
+ if (parentComponent && parentComponent.subTree === vnode) {
+ parentComponent.vnode.el = el
+ updateHOCHostEl(parentComponent, el)
+ }
+}
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index c2d13a1986d..f54beecf988 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -326,14 +326,14 @@ export function createHydrationFunctions(
const hydrateChildren = (
node: Node | null,
- vnode: VNode,
+ parentVNode: VNode,
container: Element,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
): Node | null => {
- optimized = optimized || !!vnode.dynamicChildren
- const children = vnode.children as VNode[]
+ optimized = optimized || !!parentVNode.dynamicChildren
+ const children = parentVNode.children as VNode[]
const l = children.length
let hasWarned = false
for (let i = 0; i < l; i++) {
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 94492661113..bc6cedb517b 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -250,7 +250,6 @@ import {
setCurrentRenderingInstance
} from './componentRenderUtils'
import { isVNode, normalizeVNode } from './vnode'
-import { normalizeSuspenseChildren } from './components/Suspense'
const _ssrUtils = {
createComponentInstance,
@@ -258,8 +257,7 @@ const _ssrUtils = {
renderComponentRoot,
setCurrentRenderingInstance,
isVNode,
- normalizeVNode,
- normalizeSuspenseChildren
+ normalizeVNode
}
/**
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index 5246fcb1baa..f38fc9da134 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -778,7 +778,7 @@ function baseCreateRenderer(
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
const needCallTransitionHooks =
- (!parentSuspense || (parentSuspense && parentSuspense!.isResolved)) &&
+ (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
if (needCallTransitionHooks) {
@@ -1253,14 +1253,10 @@ function baseCreateRenderer(
// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
- if (!parentSuspense) {
- if (__DEV__) warn('async setup() is used without a suspense boundary!')
- return
- }
-
- parentSuspense.registerDep(instance, setupRenderEffect)
+ parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration
+ // TODO handle self-defined fallback
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
@@ -2124,10 +2120,11 @@ function baseCreateRenderer(
if (
__FEATURE_SUSPENSE__ &&
parentSuspense &&
- !parentSuspense.isResolved &&
+ parentSuspense.pendingBranch &&
!parentSuspense.isUnmounted &&
instance.asyncDep &&
- !instance.asyncResolved
+ !instance.asyncResolved &&
+ instance.suspenseId === parentSuspense.pendingId
) {
parentSuspense.deps--
if (parentSuspense.deps === 0) {
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index a34358a9ccc..9a11090a37c 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -25,7 +25,8 @@ import { AppContext } from './apiCreateApp'
import {
SuspenseImpl,
isSuspense,
- SuspenseBoundary
+ SuspenseBoundary,
+ normalizeSuspenseChildren
} from './components/Suspense'
import { DirectiveBinding } from './directives'
import { TransitionHooks } from './components/BaseTransition'
@@ -134,7 +135,6 @@ export interface VNode<
scopeId: string | null // SFC only
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
- suspense: SuspenseBoundary | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks | null
@@ -145,6 +145,11 @@ export interface VNode<
targetAnchor: HostNode | null // teleport target anchor
staticCount: number // number of elements contained in a static vnode
+ // suspense
+ suspense: SuspenseBoundary | null
+ ssContent: VNode | null
+ ssFallback: VNode | null
+
// optimization only
shapeFlag: number
patchFlag: number
@@ -395,6 +400,8 @@ function _createVNode(
children: null,
component: null,
suspense: null,
+ ssContent: null,
+ ssFallback: null,
dirs: null,
transition: null,
el: null,
@@ -416,6 +423,13 @@ function _createVNode(
normalizeChildren(vnode, children)
+ // normalize suspense children
+ if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
+ const { content, fallback } = normalizeSuspenseChildren(vnode)
+ vnode.ssContent = content
+ vnode.ssFallback = fallback
+ }
+
if (
shouldTrack > 0 &&
// avoid a block node from tracking itself
@@ -491,6 +505,8 @@ export function cloneVNode(
// they will simply be overwritten.
component: vnode.component,
suspense: vnode.suspense,
+ ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
+ ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
el: vnode.el,
anchor: vnode.anchor
}
diff --git a/packages/runtime-dom/src/helpers/useCssVars.ts b/packages/runtime-dom/src/helpers/useCssVars.ts
index b41a0657ca3..86d571c2c35 100644
--- a/packages/runtime-dom/src/helpers/useCssVars.ts
+++ b/packages/runtime-dom/src/helpers/useCssVars.ts
@@ -40,14 +40,12 @@ function setVarsOnVNode(
prefix: string
) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
- const { isResolved, isHydrating, fallbackTree, subTree } = vnode.suspense!
- if (isResolved || isHydrating) {
- vnode = subTree
- } else {
- vnode.suspense!.effects.push(() => {
- setVarsOnVNode(subTree, vars, prefix)
+ const suspense = vnode.suspense!
+ vnode = suspense.activeBranch!
+ if (suspense.pendingBranch && !suspense.isHydrating) {
+ suspense.effects.push(() => {
+ setVarsOnVNode(suspense.activeBranch!, vars, prefix)
})
- vnode = fallbackTree
}
}
diff --git a/packages/server-renderer/src/helpers/ssrRenderSuspense.ts b/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
index 3d6df47fef7..f02b85d1796 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSuspense.ts
@@ -5,9 +5,7 @@ export async function ssrRenderSuspense(
{ default: renderContent }: Record void) | undefined>
) {
if (renderContent) {
- push(``)
renderContent()
- push(``)
} else {
push(``)
}
diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts
index 44ec2be14fe..f92e3d2bea9 100644
--- a/packages/server-renderer/src/render.ts
+++ b/packages/server-renderer/src/render.ts
@@ -33,8 +33,7 @@ const {
setCurrentRenderingInstance,
setupComponent,
renderComponentRoot,
- normalizeVNode,
- normalizeSuspenseChildren
+ normalizeVNode
} = ssrUtils
export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
@@ -200,11 +199,7 @@ export function renderVNode(
} else if (shapeFlag & ShapeFlags.TELEPORT) {
renderTeleportVNode(push, vnode, parentComponent)
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
- renderVNode(
- push,
- normalizeSuspenseChildren(vnode).content,
- parentComponent
- )
+ renderVNode(push, vnode.ssContent!, parentComponent)
} else {
warn(
'[@vue/server-renderer] Invalid VNode type:',
diff --git a/packages/vue/__tests__/Transition.spec.ts b/packages/vue/__tests__/Transition.spec.ts
index 70f038609ba..93959e45d50 100644
--- a/packages/vue/__tests__/Transition.spec.ts
+++ b/packages/vue/__tests__/Transition.spec.ts
@@ -1115,12 +1115,11 @@ describe('e2e: Transition', () => {
createApp({
template: `
-
-
+
+
content
-
-
+
+
button
`,
@@ -1138,6 +1137,13 @@ describe('e2e: Transition', () => {
}
}).mount('#app')
})
+
+ expect(onEnterSpy).toBeCalledTimes(1)
+ await nextFrame()
+ expect(await html('#container')).toBe(
+ 'content
'
+ )
+ await transitionFinish()
expect(await html('#container')).toBe('content
')
// leave
@@ -1174,7 +1180,7 @@ describe('e2e: Transition', () => {
'v-enter-active',
'v-enter-from'
])
- expect(onEnterSpy).toBeCalledTimes(1)
+ expect(onEnterSpy).toBeCalledTimes(2)
await nextFrame()
expect(await classList('.test')).toStrictEqual([
'test',
@@ -1196,11 +1202,11 @@ describe('e2e: Transition', () => {
createApp({
template: `
button
`,
@@ -1245,6 +1251,71 @@ describe('e2e: Transition', () => {
},
E2E_TIMEOUT
)
+
+ test(
+ 'out-in mode with Suspense',
+ async () => {
+ const onLeaveSpy = jest.fn()
+ const onEnterSpy = jest.fn()
+
+ await page().exposeFunction('onLeaveSpy', onLeaveSpy)
+ await page().exposeFunction('onEnterSpy', onEnterSpy)
+
+ await page().evaluate(() => {
+ const { createApp, shallowRef, h } = (window as any).Vue
+ const One = {
+ async setup() {
+ return () => h('div', { class: 'test' }, 'one')
+ }
+ }
+ const Two = {
+ async setup() {
+ return () => h('div', { class: 'test' }, 'two')
+ }
+ }
+ createApp({
+ template: `
+
+
+
+
+
+
+
+ button
+ `,
+ setup: () => {
+ const view = shallowRef(One)
+ const click = () => {
+ view.value = view.value === One ? Two : One
+ }
+ return { view, click }
+ }
+ }).mount('#app')
+ })
+
+ await nextFrame()
+ expect(await html('#container')).toBe(
+ 'one
'
+ )
+ await transitionFinish()
+ expect(await html('#container')).toBe('one
')
+
+ // leave
+ await classWhenTransitionStart()
+ await nextFrame()
+ expect(await html('#container')).toBe(
+ 'one
'
+ )
+ await transitionFinish()
+ expect(await html('#container')).toBe(
+ 'two
'
+ )
+ await transitionFinish()
+ expect(await html('#container')).toBe('two
')
+ },
+ E2E_TIMEOUT
+ )
})
describe('transition with v-show', () => {
diff --git a/packages/vue/examples/classic/markdown.html b/packages/vue/examples/classic/markdown.html
index 3d9126dd338..da6e5baa449 100644
--- a/packages/vue/examples/classic/markdown.html
+++ b/packages/vue/examples/classic/markdown.html
@@ -8,7 +8,7 @@