瀏覽代碼

fix(hydration): prevent lazy hydration for updated components (#13511)

close #13510
edison 9 月之前
父節點
當前提交
a9269c642b
共有 2 個文件被更改,包括 78 次插入13 次删除
  1. 63 0
      packages/runtime-core/__tests__/hydration.spec.ts
  2. 15 13
      packages/runtime-core/src/apiAsyncComponent.ts

+ 63 - 0
packages/runtime-core/__tests__/hydration.spec.ts

@@ -1160,6 +1160,69 @@ describe('SSR hydration', () => {
     )
   })
 
+  // #13510
+  test('update async component after parent mount before async component resolve', async () => {
+    const Comp = {
+      props: ['toggle'],
+      render(this: any) {
+        return h('h1', [
+          this.toggle ? 'Async component' : 'Updated async component',
+        ])
+      },
+    }
+    let serverResolve: any
+    let AsyncComp = defineAsyncComponent(
+      () =>
+        new Promise(r => {
+          serverResolve = r
+        }),
+    )
+
+    const toggle = ref(true)
+    const App = {
+      setup() {
+        onMounted(() => {
+          // change state, after mount and before async component resolve
+          nextTick(() => (toggle.value = false))
+        })
+
+        return () => {
+          return h(AsyncComp, { toggle: toggle.value })
+        }
+      },
+    }
+
+    // server render
+    const htmlPromise = renderToString(h(App))
+    serverResolve(Comp)
+    const html = await htmlPromise
+    expect(html).toMatchInlineSnapshot(`"<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))
+
+    // prevent lazy hydration since the component has been patched
+    expect('Skipping lazy hydration for component').toHaveBeenWarned()
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<h1>Updated async component</h1>"`,
+    )
+  })
+
   test('hydrate safely when property used by async setup changed before render', async () => {
     const toggle = ref(true)
 

+ 15 - 13
packages/runtime-core/src/apiAsyncComponent.ts

@@ -123,28 +123,30 @@ export function defineAsyncComponent<
 
     __asyncHydrate(el, instance, hydrate) {
       let patched = false
+      ;(instance.bu || (instance.bu = [])).push(() => (patched = true))
+      const performHydrate = () => {
+        // skip hydration if the component has been patched
+        if (patched) {
+          if (__DEV__) {
+            warn(
+              `Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
+                `it was updated before lazy hydration performed.`,
+            )
+          }
+          return
+        }
+        hydrate()
+      }
       const doHydrate = hydrateStrategy
         ? () => {
-            const performHydrate = () => {
-              // skip hydration if the component has been patched
-              if (__DEV__ && patched) {
-                warn(
-                  `Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
-                    `it was updated before lazy hydration performed.`,
-                )
-                return
-              }
-              hydrate()
-            }
             const teardown = hydrateStrategy(performHydrate, cb =>
               forEachElement(el, cb),
             )
             if (teardown) {
               ;(instance.bum || (instance.bum = [])).push(teardown)
             }
-            ;(instance.u || (instance.u = [])).push(() => (patched = true))
           }
-        : hydrate
+        : performHydrate
       if (resolvedComp) {
         doHydrate()
       } else {