]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): handle hydrated async component unmounted before resolve
authorEvan You <yyx990803@gmail.com>
Wed, 26 May 2021 19:26:18 +0000 (15:26 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 26 May 2021 19:26:18 +0000 (15:26 -0400)
fix #3787

packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/renderer.ts

index 8db566b5e51b004d5f8a9ae4b66353f220c048c9..c55a9ab6104b190b1497fed02387d1be6b5128eb 100644 (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>',
index 45b6d7b032964d239042b34d99551dc240bc722f..f4f7ea4795ac8fc7d33645d48d6eae2771e4e79f 100644 (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()
index 4e661bc9720024dd2631189a9dcaf24879fa9e39..1da8cd09720648ab5cc93fc628cdf49a7ba1b7eb 100644 (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()