Skip to content

Commit

Permalink
fix(suspense): fix nested suspensible suspense with no asyn deps
Browse files Browse the repository at this point in the history
close #8206
  • Loading branch information
yyx990803 committed May 8, 2023
1 parent 62e71b5 commit e147512
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 11 deletions.
131 changes: 127 additions & 4 deletions packages/runtime-core/__tests__/components/Suspense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
shallowRef,
Fragment
} from '@vue/runtime-test'
import { createApp } from 'vue'
import { createApp, defineComponent } from 'vue'

describe('Suspense', () => {
const deps: Promise<any>[] = []
Expand Down Expand Up @@ -1335,9 +1335,14 @@ describe('Suspense', () => {
h(Suspense, null, {
default: [
h(outerToggle.value ? OuterB : OuterA, null, {
default: () => h(Suspense, { suspensible: true },{
default: h(innerToggle.value ? InnerB : InnerA)
})
default: () =>
h(
Suspense,
{ suspensible: true },
{
default: h(innerToggle.value ? InnerB : InnerA)
}
)
})
],
fallback: h('div', 'fallback outer')
Expand Down Expand Up @@ -1400,4 +1405,122 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(expected)
expect(calls).toContain('innerB mounted')
})

// #8206
test('nested suspense with suspensible & no async deps', async () => {
const calls: string[] = []
let expected = ''

const InnerA = defineComponent({
setup: () => {
calls.push('innerA created')
onMounted(() => {
calls.push('innerA mounted')
})
return () => h('div', 'innerA')
}
})

const InnerB = defineComponent({
setup: () => {
calls.push('innerB created')
onMounted(() => {
calls.push('innerB mounted')
})
return () => h('div', 'innerB')
}
})

const OuterA = defineComponent({
setup: (_, { slots }: any) => {
calls.push('outerA created')
onMounted(() => {
calls.push('outerA mounted')
})
return () => h(Fragment, null, [h('div', 'outerA'), slots.default?.()])
}
})

const OuterB = defineComponent({
setup: (_, { slots }: any) => {
calls.push('outerB created')
onMounted(() => {
calls.push('outerB mounted')
})
return () => h(Fragment, null, [h('div', 'outerB'), slots.default?.()])
}
})

const outerToggle = ref(false)
const innerToggle = ref(false)

/**
* <Suspense>
* <component :is="outerToggle ? outerB : outerA">
* <Suspense suspensible>
* <component :is="innerToggle ? innerB : innerA" />
* </Suspense>
* </component>
* </Suspense>
*/
const Comp = defineComponent({
setup() {
return () =>
h(Suspense, null, {
default: [
h(outerToggle.value ? OuterB : OuterA, null, {
default: () =>
h(
Suspense,
{ suspensible: true },
{
default: h(innerToggle.value ? InnerB : InnerA)
}
)
})
],
fallback: h('div', 'fallback outer')
})
}
})

expected = `<div>outerA</div><div>innerA</div>`
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(expected)

// mount outer component and inner component
await Promise.all(deps)
await nextTick()

expect(serializeInner(root)).toBe(expected)
expect(calls).toEqual([
'outerA created',
'innerA created',
'innerA mounted',
'outerA mounted'
])

// toggle outer component
calls.length = 0
deps.length = 0
outerToggle.value = true
await nextTick()

await Promise.all(deps)
await nextTick()
expected = `<div>outerB</div><div>innerA</div>`
expect(serializeInner(root)).toBe(expected)
expect(calls).toContain('outerB mounted')
expect(calls).toContain('innerA mounted')

// toggle inner component
calls.length = 0
deps.length = 0
innerToggle.value = true
await Promise.all(deps)
await nextTick()
expected = `<div>outerB</div><div>innerB</div>`
expect(serializeInner(root)).toBe(expected)
})
})
17 changes: 10 additions & 7 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ function mountSuspense(
setActiveBranch(suspense, vnode.ssFallback!)
} else {
// Suspense has no async deps. Just resolve.
suspense.resolve()
suspense.resolve(false, true)
}
}

Expand Down Expand Up @@ -388,7 +388,7 @@ export interface SuspenseBoundary {
isHydrating: boolean
isUnmounted: boolean
effects: Function[]
resolve(force?: boolean): void
resolve(force?: boolean, sync?: boolean): void
fallback(fallbackVNode: VNode): void
move(
container: RendererElement,
Expand Down Expand Up @@ -437,11 +437,10 @@ function createSuspenseBoundary(

// if set `suspensible: true`, set the current suspense as a dep of parent suspense
let parentSuspenseId: number | undefined
const isSuspensible =
vnode.props?.suspensible != null && vnode.props.suspensible !== false
const isSuspensible = isVNodeSuspensible(vnode)
if (isSuspensible) {
if (parentSuspense?.pendingBranch) {
parentSuspenseId = parentSuspense?.pendingId
parentSuspenseId = parentSuspense.pendingId
parentSuspense.deps++
}
}
Expand Down Expand Up @@ -469,7 +468,7 @@ function createSuspenseBoundary(
isUnmounted: false,
effects: [],

resolve(resume = false) {
resolve(resume = false, sync = false) {
if (__DEV__) {
if (!resume && !suspense.pendingBranch) {
throw new Error(
Expand Down Expand Up @@ -553,7 +552,7 @@ function createSuspenseBoundary(
parentSuspenseId === parentSuspense.pendingId
) {
parentSuspense.deps--
if (parentSuspense.deps === 0) {
if (parentSuspense.deps === 0 && !sync) {
parentSuspense.resolve()
}
}
Expand Down Expand Up @@ -831,3 +830,7 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
updateHOCHostEl(parentComponent, el)
}
}

function isVNodeSuspensible(vnode: VNode) {
return vnode.props?.suspensible != null && vnode.props.suspensible !== false
}

0 comments on commit e147512

Please sign in to comment.