Просмотр исходного кода

fix(hydration): avoid component double mount during hydration (#14690)

edison 2 недель назад
Родитель
Сommit
340af2c2a6

+ 101 - 4
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -383,6 +383,23 @@ describe('Vapor Mode hydration', () => {
   })
 
   describe('component', () => {
+    test('root component should not enter beforeMount twice during hydration', async () => {
+      const beforeMount = vi.fn()
+      await testHydration(
+        `
+          <script vapor>
+            import { onBeforeMount } from 'vue'
+            const data = _data
+            onBeforeMount(() => data.value.beforeMount())
+          </script>
+          <template><div>root</div></template>
+        `,
+        {},
+        ref({ beforeMount }),
+      )
+      expect(beforeMount).toHaveBeenCalledTimes(1)
+    })
+
     test('basic component', async () => {
       const { container, data } = await testHydration(
         `
@@ -4675,11 +4692,91 @@ describe('Vapor Mode hydration', () => {
       )
     })
 
-    // required vapor Suspense
-    test.todo('hydrate safely when property used by async setup changed before render', async () => {})
+    describe('suspense', () => {
+      describe('VDOM suspense', () => {
+        test('hydrate VDOM Suspense vapor async setup should not enter mount hooks twice', async () => {
+          const beforeMount = vi.fn()
+          const data = ref({ beforeMount })
+          const vaporChildCode = `
+            <script vapor>
+              import { onBeforeMount } from 'vue'
+              const data = _data
+              onBeforeMount(() => data.value.beforeMount())
+              await new Promise(r => setTimeout(r, 10))
+            </script>
+            <template><h1>Async component</h1></template>
+          `
+          const appCode = `
+            <script setup>
+              import { Suspense } from 'vue'
+              const components = _components
+            </script>
+            <template>
+              <Suspense>
+                <components.VaporChild />
+              </Suspense>
+            </template>
+          `
+
+          const serverComponents: any = {}
+          const clientComponents: any = {}
+          serverComponents.VaporChild = compile(
+            vaporChildCode,
+            data,
+            serverComponents,
+            {
+              vapor: true,
+              ssr: true,
+            },
+          )
+          clientComponents.VaporChild = compile(
+            vaporChildCode,
+            data,
+            clientComponents,
+            {
+              vapor: true,
+              ssr: false,
+            },
+          )
+
+          const serverApp = compile(appCode, data, serverComponents, {
+            vapor: false,
+            ssr: true,
+          })
+          const html = await VueServerRenderer.renderToString(
+            runtimeDom.createSSRApp(serverApp),
+          )
+
+          const clientApp = compile(appCode, data, clientComponents, {
+            vapor: false,
+            ssr: false,
+          })
 
-    // required vapor Suspense
-    test.todo('hydrate safely when property used by deep nested async setup changed before render', async () => {})
+          const container = document.createElement('div')
+          container.innerHTML = html
+          document.body.appendChild(container)
+
+          const app = runtimeDom.createSSRApp(clientApp)
+          app.use(runtimeVapor.vaporInteropPlugin)
+
+          app.mount(container)
+          await new Promise(r => setTimeout(r, 10))
+          await nextTick()
+
+          // beforeMount should only be called once during hydration
+          expect(beforeMount).toHaveBeenCalledTimes(1)
+        })
+
+        test.todo('hydrate safely when property used by async setup changed before render', async () => {})
+        test.todo('hydrate safely when property used by deep nested async setup changed before render', async () => {})
+      })
+
+      // required vapor Suspense
+      describe.todo('vapor suspense', () => {
+        test.todo('hydrate safely when property used by async setup changed before render', async () => {})
+        test.todo('hydrate safely when property used by deep nested async setup changed before render', async () => {})
+      })
+    })
 
     test('unmount async wrapper before load', async () => {
       const data = ref({

+ 1 - 0
packages/runtime-vapor/src/apiCreateApp.ts

@@ -68,6 +68,7 @@ const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
         false,
         false,
         app._context,
+        true,
       )
     mountComponent(instance, container)
     flushOnAppMount()

+ 2 - 1
packages/runtime-vapor/src/component.ts

@@ -239,6 +239,7 @@ export function createComponent(
   appContext: GenericAppContext = (currentInstance &&
     currentInstance.appContext) ||
     emptyContext,
+  managedMount = false,
 ): VaporComponentInstance {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
@@ -384,7 +385,7 @@ export function createComponent(
   setCurrentSlotOwner(prevSlotOwner)
   onScopeDispose(() => unmountComponent(instance), true)
 
-  if (_insertionParent || isHydrating) {
+  if (!managedMount && (_insertionParent || isHydrating)) {
     mountComponent(instance, _insertionParent!, _insertionAnchor)
   }
 

+ 2 - 0
packages/runtime-vapor/src/vdomInterop.ts

@@ -182,6 +182,8 @@ const vaporInteropImpl: Omit<
       undefined,
       undefined,
       (parentComponent ? parentComponent.appContext : vnode.appContext) as any,
+      // VDOM interop owns the explicit mount below
+      true,
     ))
     instance.rawPropsRef = propsRef
     instance.rawSlotsRef = slotsRef