fix(ssr): handle hydrated async component unmounted before resolve
fix #3787
This commit is contained in:
parent
b57e995edd
commit
b46a4dccf6
@ -626,7 +626,7 @@ describe('SSR hydration', () => {
|
|||||||
expect(spy).toHaveBeenCalled()
|
expect(spy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => {
|
test('update async wrapper before resolve', async () => {
|
||||||
const Comp = {
|
const Comp = {
|
||||||
render() {
|
render() {
|
||||||
return h('h1', 'Async component')
|
return h('h1', 'Async component')
|
||||||
@ -687,6 +687,57 @@ describe('SSR hydration', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// #3787
|
||||||
|
test('unmount async wrapper before load', async () => {
|
||||||
|
let resolve: any
|
||||||
|
const AsyncComp = defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
new Promise(r => {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const show = ref(true)
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.innerHTML = '<div><div>async</div></div>'
|
||||||
|
|
||||||
|
createSSRApp({
|
||||||
|
render() {
|
||||||
|
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
|
||||||
|
}
|
||||||
|
}).mount(root)
|
||||||
|
|
||||||
|
show.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
|
||||||
|
resolve({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unmount async wrapper before load (fragment)', async () => {
|
||||||
|
let resolve: any
|
||||||
|
const AsyncComp = defineAsyncComponent(
|
||||||
|
() =>
|
||||||
|
new Promise(r => {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const show = ref(true)
|
||||||
|
const root = document.createElement('div')
|
||||||
|
root.innerHTML = '<div><!--[-->async<!--]--></div>'
|
||||||
|
|
||||||
|
createSSRApp({
|
||||||
|
render() {
|
||||||
|
return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
|
||||||
|
}
|
||||||
|
}).mount(root)
|
||||||
|
|
||||||
|
show.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(root.innerHTML).toBe('<div><div>hi</div></div>')
|
||||||
|
resolve({})
|
||||||
|
})
|
||||||
|
|
||||||
test('elements with camel-case in svg ', () => {
|
test('elements with camel-case in svg ', () => {
|
||||||
const { vnode, container } = mountWithHydration(
|
const { vnode, container } = mountWithHydration(
|
||||||
'<animateTransform></animateTransform>',
|
'<animateTransform></animateTransform>',
|
||||||
|
@ -5,7 +5,9 @@ import {
|
|||||||
Comment,
|
Comment,
|
||||||
Static,
|
Static,
|
||||||
Fragment,
|
Fragment,
|
||||||
VNodeHook
|
VNodeHook,
|
||||||
|
createVNode,
|
||||||
|
createTextVNode
|
||||||
} from './vnode'
|
} from './vnode'
|
||||||
import { flushPostFlushCbs } from './scheduler'
|
import { flushPostFlushCbs } from './scheduler'
|
||||||
import { ComponentInternalInstance } from './component'
|
import { ComponentInternalInstance } from './component'
|
||||||
@ -19,6 +21,7 @@ import {
|
|||||||
queueEffectWithSuspense
|
queueEffectWithSuspense
|
||||||
} from './components/Suspense'
|
} from './components/Suspense'
|
||||||
import { TeleportImpl, TeleportVNode } from './components/Teleport'
|
import { TeleportImpl, TeleportVNode } from './components/Teleport'
|
||||||
|
import { isAsyncWrapper } from './apiAsyncComponent'
|
||||||
|
|
||||||
export type RootHydrateFunction = (
|
export type RootHydrateFunction = (
|
||||||
vnode: VNode<Node, Element>,
|
vnode: VNode<Node, Element>,
|
||||||
@ -187,12 +190,32 @@ export function createHydrationFunctions(
|
|||||||
isSVGContainer(container),
|
isSVGContainer(container),
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
|
|
||||||
// component may be async, so in the case of fragments we cannot rely
|
// component may be async, so in the case of fragments we cannot rely
|
||||||
// on component's rendered output to determine the end of the fragment
|
// on component's rendered output to determine the end of the fragment
|
||||||
// instead, we do a lookahead to find the end anchor node.
|
// instead, we do a lookahead to find the end anchor node.
|
||||||
nextNode = isFragmentStart
|
nextNode = isFragmentStart
|
||||||
? locateClosingAsyncAnchor(node)
|
? locateClosingAsyncAnchor(node)
|
||||||
: nextSibling(node)
|
: nextSibling(node)
|
||||||
|
|
||||||
|
// #3787
|
||||||
|
// if component is async, it may get moved / unmounted before its
|
||||||
|
// inner component is loaded, so we need to give it a placeholder
|
||||||
|
// vnode that matches its adopted DOM.
|
||||||
|
if (isAsyncWrapper(vnode)) {
|
||||||
|
let subTree
|
||||||
|
if (isFragmentStart) {
|
||||||
|
subTree = createVNode(Fragment)
|
||||||
|
subTree.anchor = nextNode
|
||||||
|
? nextNode.previousSibling
|
||||||
|
: container.lastChild
|
||||||
|
} else {
|
||||||
|
subTree =
|
||||||
|
node.nodeType === 3 ? createTextVNode('') : createVNode('div')
|
||||||
|
}
|
||||||
|
subTree.el = node
|
||||||
|
vnode.component!.subTree = subTree
|
||||||
|
}
|
||||||
} else if (shapeFlag & ShapeFlags.TELEPORT) {
|
} else if (shapeFlag & ShapeFlags.TELEPORT) {
|
||||||
if (domType !== DOMNodeTypes.COMMENT) {
|
if (domType !== DOMNodeTypes.COMMENT) {
|
||||||
nextNode = onMismatch()
|
nextNode = onMismatch()
|
||||||
|
@ -1462,7 +1462,7 @@ function baseCreateRenderer(
|
|||||||
// which means it won't track dependencies - but it's ok because
|
// which means it won't track dependencies - but it's ok because
|
||||||
// a server-rendered async wrapper is already in resolved state
|
// a server-rendered async wrapper is already in resolved state
|
||||||
// and it will never need to change.
|
// and it will never need to change.
|
||||||
hydrateSubTree
|
() => !instance.isUnmounted && hydrateSubTree()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
hydrateSubTree()
|
hydrateSubTree()
|
||||||
|
Loading…
Reference in New Issue
Block a user