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