fix(ssr): handle hydrated async component unmounted before resolve

fix #3787
This commit is contained in:
Evan You 2021-05-26 15:26:18 -04:00
parent b57e995edd
commit b46a4dccf6
3 changed files with 77 additions and 3 deletions

View File

@ -626,7 +626,7 @@ describe('SSR hydration', () => {
expect(spy).toHaveBeenCalled()
})
test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => {
test('update async wrapper before resolve', async () => {
const Comp = {
render() {
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 ', () => {
const { vnode, container } = mountWithHydration(
'<animateTransform></animateTransform>',

View File

@ -5,7 +5,9 @@ import {
Comment,
Static,
Fragment,
VNodeHook
VNodeHook,
createVNode,
createTextVNode
} from './vnode'
import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
@ -19,6 +21,7 @@ import {
queueEffectWithSuspense
} from './components/Suspense'
import { TeleportImpl, TeleportVNode } from './components/Teleport'
import { isAsyncWrapper } from './apiAsyncComponent'
export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
@ -187,12 +190,32 @@ export function createHydrationFunctions(
isSVGContainer(container),
optimized
)
// 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
// instead, we do a lookahead to find the end anchor node.
nextNode = isFragmentStart
? locateClosingAsyncAnchor(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) {
if (domType !== DOMNodeTypes.COMMENT) {
nextNode = onMismatch()

View File

@ -1462,7 +1462,7 @@ function baseCreateRenderer(
// which means it won't track dependencies - but it's ok because
// a server-rendered async wrapper is already in resolved state
// and it will never need to change.
hydrateSubTree
() => !instance.isUnmounted && hydrateSubTree()
)
} else {
hydrateSubTree()