]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hydration): fix update before async component is hydrated (#3563)
authorHcySunYang <HcySunYang@outlook.com>
Fri, 7 May 2021 22:42:58 +0000 (06:42 +0800)
committerGitHub <noreply@github.com>
Fri, 7 May 2021 22:42:58 +0000 (18:42 -0400)
fix #3560

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

index a12131454a6f530d49e228d5503380f5cab9ba56..8db566b5e51b004d5f8a9ae4b66353f220c048c9 100644 (file)
@@ -626,6 +626,67 @@ describe('SSR hydration', () => {
     expect(spy).toHaveBeenCalled()
   })
 
+  test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => {
+    const Comp = {
+      render() {
+        return h('h1', 'Async component')
+      }
+    }
+    let serverResolve: any
+    let AsyncComp = defineAsyncComponent(
+      () =>
+        new Promise(r => {
+          serverResolve = r
+        })
+    )
+
+    const bol = ref(true)
+    const App = {
+      setup() {
+        onMounted(() => {
+          // change state, this makes updateComponent(AsyncComp) execute before
+          // the async component is resolved
+          bol.value = false
+        })
+
+        return () => {
+          return [bol.value ? 'hello' : 'world', h(AsyncComp)]
+        }
+      }
+    }
+
+    // server render
+    const htmlPromise = renderToString(h(App))
+    serverResolve(Comp)
+    const html = await htmlPromise
+    expect(html).toMatchInlineSnapshot(
+      `"<!--[-->hello<h1>Async component</h1><!--]-->"`
+    )
+
+    // hydration
+    let clientResolve: any
+    AsyncComp = defineAsyncComponent(
+      () =>
+        new Promise(r => {
+          clientResolve = r
+        })
+    )
+
+    const container = document.createElement('div')
+    container.innerHTML = html
+    createSSRApp(App).mount(container)
+
+    // resolve
+    clientResolve(Comp)
+    await new Promise(r => setTimeout(r))
+
+    // should be hydrated now
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[-->world<h1>Async component</h1><!--]-->"`
+    )
+  })
+
   test('elements with camel-case in svg ', () => {
     const { vnode, container } = mountWithHydration(
       '<animateTransform></animateTransform>',
index 0085ce6eea131c9dac92ecacb1c41222bcfe03c9..45b6d7b032964d239042b34d99551dc240bc722f 100644 (file)
@@ -8,7 +8,7 @@ import {
   VNodeHook
 } from './vnode'
 import { flushPostFlushCbs } from './scheduler'
-import { ComponentOptions, ComponentInternalInstance } from './component'
+import { ComponentInternalInstance } from './component'
 import { invokeDirectiveHook } from './directives'
 import { warn } from './warning'
 import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
@@ -178,24 +178,15 @@ export function createHydrationFunctions(
           // on its sub-tree.
           vnode.slotScopeIds = slotScopeIds
           const container = parentNode(node)!
-          const hydrateComponent = () => {
-            mountComponent(
-              vnode,
-              container,
-              null,
-              parentComponent,
-              parentSuspense,
-              isSVGContainer(container),
-              optimized
-            )
-          }
-          // async component
-          const loadAsync = (vnode.type as ComponentOptions).__asyncLoader
-          if (loadAsync) {
-            loadAsync().then(hydrateComponent)
-          } else {
-            hydrateComponent()
-          }
+          mountComponent(
+            vnode,
+            container,
+            null,
+            parentComponent,
+            parentSuspense,
+            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.
index c075b3e5abe86e8bc72e81e550549f5b8d5040c1..4e661bc9720024dd2631189a9dcaf24879fa9e39 100644 (file)
@@ -16,6 +16,7 @@ import {
 } from './vnode'
 import {
   ComponentInternalInstance,
+  ComponentOptions,
   createComponentInstance,
   Data,
   setupComponent
@@ -1430,31 +1431,50 @@ function baseCreateRenderer(
           instance.emit('hook:beforeMount')
         }
 
-        // render
-        if (__DEV__) {
-          startMeasure(instance, `render`)
-        }
-        const subTree = (instance.subTree = renderComponentRoot(instance))
-        if (__DEV__) {
-          endMeasure(instance, `render`)
-        }
-
         if (el && hydrateNode) {
+          // vnode has adopted host node - perform hydration instead of mount.
+          const hydrateSubTree = () => {
+            if (__DEV__) {
+              startMeasure(instance, `render`)
+            }
+            instance.subTree = renderComponentRoot(instance)
+            if (__DEV__) {
+              endMeasure(instance, `render`)
+            }
+            if (__DEV__) {
+              startMeasure(instance, `hydrate`)
+            }
+            hydrateNode!(
+              el as Node,
+              instance.subTree,
+              instance,
+              parentSuspense,
+              null
+            )
+            if (__DEV__) {
+              endMeasure(instance, `hydrate`)
+            }
+          }
+
+          if (isAsyncWrapper(initialVNode)) {
+            (initialVNode.type as ComponentOptions).__asyncLoader!().then(
+              // note: we are moving the render call into an async callback,
+              // 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
+            )
+          } else {
+            hydrateSubTree()
+          }
+        } else {
           if (__DEV__) {
-            startMeasure(instance, `hydrate`)
+            startMeasure(instance, `render`)
           }
-          // vnode has adopted host node - perform hydration instead of mount.
-          hydrateNode(
-            initialVNode.el as Node,
-            subTree,
-            instance,
-            parentSuspense,
-            null
-          )
+          const subTree = (instance.subTree = renderComponentRoot(instance))
           if (__DEV__) {
-            endMeasure(instance, `hydrate`)
+            endMeasure(instance, `render`)
           }
-        } else {
           if (__DEV__) {
             startMeasure(instance, `patch`)
           }