]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: vdom interop
authordaiwei <daiwei521@126.com>
Mon, 28 Apr 2025 03:06:04 +0000 (11:06 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 28 Apr 2025 07:21:18 +0000 (15:21 +0800)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/vdomInterop.ts

index d8ae73fb69d1bbf3614458c8651d202c55ed50f9..63690d0cea4cf43d6d13f3ef0de7389255fc9b49 100644 (file)
@@ -33,6 +33,7 @@ import type { NormalizedPropsOptions } from './componentProps'
 import type { ObjectEmitsOptions } from './componentEmits'
 import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
 import type { DefineComponent } from './apiDefineComponent'
+import type { createHydrationFunctions } from './hydration'
 
 export interface App<HostElement = any> {
   version: string
@@ -104,6 +105,7 @@ export interface App<HostElement = any> {
   _container: HostElement | null
   _context: AppContext
   _instance: GenericComponentInstance | null
+  _ssr?: boolean
 
   /**
    * @internal custom element vnode
@@ -193,6 +195,7 @@ export interface VaporInteropInterface {
   unmount(vnode: VNode, doRemove?: boolean): void
   move(vnode: VNode, container: any, anchor: any): void
   slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
+  hydrate(node: Node, fn: () => void): void
 
   vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
   vdomUnmount: UnmountComponentFn
@@ -203,6 +206,7 @@ export interface VaporInteropInterface {
     parentComponent: any, // VaporComponentInstance
     fallback?: any, // VaporSlot
   ) => any
+  vdomHydrate: ReturnType<typeof createHydrationFunctions>[1] | undefined
 }
 
 /**
index 6c702f683832c15cdf37ad9874441956d29c9176..e1420d33536619b088e692ce7d18fa4319e868c8 100644 (file)
@@ -37,7 +37,11 @@ import {
   normalizeStyle,
   stringifyStyle,
 } from '@vue/shared'
-import { type RendererInternals, needTransition } from './renderer'
+import {
+  type RendererInternals,
+  getVaporInterface,
+  needTransition,
+} from './renderer'
 import { setRef } from './rendererTemplateRef'
 import {
   type SuspenseBoundary,
@@ -294,10 +298,6 @@ export function createHydrationFunctions(
             )
           }
         } else if (shapeFlag & ShapeFlags.COMPONENT) {
-          if ((vnode.type as ConcreteComponent).__vapor) {
-            throw new Error('Vapor component hydration is not supported yet.')
-          }
-
           // when setting up the render effect, if the initial vnode already
           // has .el set, the component will perform hydration instead of mount
           // on its sub-tree.
@@ -318,15 +318,23 @@ export function createHydrationFunctions(
             nextNode = nextSibling(node)
           }
 
-          mountComponent(
-            vnode,
-            container,
-            null,
-            parentComponent,
-            parentSuspense,
-            getContainerType(container),
-            optimized,
-          )
+          // hydrate vapor component
+          if ((vnode.type as ConcreteComponent).__vapor) {
+            const vaporInterface = getVaporInterface(parentComponent, vnode)
+            vaporInterface.hydrate(node, () => {
+              vaporInterface.mount(vnode, container, null, parentComponent)
+            })
+          } else {
+            mountComponent(
+              vnode,
+              container,
+              null,
+              parentComponent,
+              parentSuspense,
+              getContainerType(container),
+              optimized,
+            )
+          }
 
           // #3787
           // if component is async, it may get moved / unmounted before its
index 5a18d62a8e1b2d6f74bdd814217bf482e4fa8ac2..fade2a4b393e020143de9f71190bf8c0bbc61a27 100644 (file)
@@ -107,6 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
 
 export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
   hydrate: RootHydrateFunction
+  hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
 }
 
 export type ElementNamespace = 'svg' | 'mathml' | undefined
@@ -2524,6 +2525,7 @@ function baseCreateRenderer(
   return {
     render,
     hydrate,
+    hydrateNode,
     internals,
     createApp: createAppAPI(
       mountApp,
@@ -2639,7 +2641,10 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
   }
 }
 
-function getVaporInterface(
+/**
+ * @internal
+ */
+export function getVaporInterface(
   instance: ComponentInternalInstance | null,
   vnode: VNode,
 ): VaporInteropInterface {
index 51c72fe2ed16f5f78dfedcb371f673e5acdb3d73..14387e477597edf544d734c6769d91a156e47bfd 100644 (file)
@@ -149,6 +149,7 @@ export const createApp = ((...args) => {
 
 export const createSSRApp = ((...args) => {
   const app = ensureHydrationRenderer().createApp(...args)
+  app._ssr = true
 
   if (__DEV__) {
     injectNativeTagCheck(app)
@@ -319,7 +320,7 @@ export * from './jsx'
 /**
  * @internal
  */
-export { ensureRenderer, normalizeContainer }
+export { ensureRenderer, ensureHydrationRenderer, normalizeContainer }
 /**
  * @internal
  */
index 03675475b8d981c4c5ca757fa2c0b804c16ec7ed..939555ea66c8136f9cdcf3e6ff9476b0c11099ca 100644 (file)
@@ -58,7 +58,11 @@ import {
   getSlot,
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  currentHydrationNode,
+  isHydrating,
+  locateHydrationNode,
+} from './dom/hydration'
 import {
   insertionAnchor,
   insertionParent,
@@ -152,13 +156,22 @@ export function createComponent(
 
   // vdom interop enabled and component is not an explicit vapor component
   if (appContext.vapor && !component.__vapor) {
-    const frag = appContext.vapor.vdomMount(
+    const [frag, vnode] = appContext.vapor.vdomMount(
       component as any,
       rawProps,
       rawSlots,
     )
     if (!isHydrating && _insertionParent) {
       insert(frag, _insertionParent, _insertionAnchor)
+    } else if (isHydrating) {
+      appContext.vapor.vdomHydrate!(
+        currentHydrationNode!,
+        vnode,
+        currentInstance as any,
+        null,
+        null,
+        false,
+      )
     }
     return frag
   }
index 3ed3ee2b8671f579d99c854b5189bafb83467958..9b2e0efa749337c41833352f15c4a4bff86c5b54 100644 (file)
@@ -22,10 +22,15 @@ export function setCurrentHydrationNode(node: Node | null): void {
 
 let isOptimized = false
 
-export function withHydration(container: ParentNode, fn: () => void): void {
-  adoptTemplate = adoptTemplateImpl
-  locateHydrationNode = locateHydrationNodeImpl
+function performHydration<T>(
+  fn: () => T,
+  setup: () => void,
+  cleanup: () => void,
+): T {
   if (!isOptimized) {
+    adoptTemplate = adoptTemplateImpl
+    locateHydrationNode = locateHydrationNodeImpl
+
     // optimize anchor cache lookup
     ;(Comment.prototype as any).$fs = undefined
     ;(Node.prototype as any).$nc = undefined
@@ -33,15 +38,27 @@ export function withHydration(container: ParentNode, fn: () => void): void {
   }
   enableHydrationNodeLookup()
   isHydrating = true
-  setInsertionState(container, 0)
+  setup()
   const res = fn()
-  resetInsertionState()
+  cleanup()
   currentHydrationNode = null
   isHydrating = false
   disableHydrationNodeLookup()
   return res
 }
 
+export function withHydration(container: ParentNode, fn: () => void): void {
+  const setup = () => setInsertionState(container, 0)
+  const cleanup = () => resetInsertionState()
+  return performHydration(fn, setup, cleanup)
+}
+
+export function hydrateNode(node: Node, fn: () => void): void {
+  const setup = () => (currentHydrationNode = node)
+  const cleanup = () => {}
+  return performHydration(fn, setup, cleanup)
+}
+
 export let adoptTemplate: (node: Node, template: string) => Node | null
 export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
 
index 77228fd72a02fe85a5496daf7d89bc37e197a4d2..6f3bc6c69d5fb7f9a5ba6cd887ad0ae779e29865 100644 (file)
@@ -2,6 +2,7 @@ import {
   type App,
   type ComponentInternalInstance,
   type ConcreteComponent,
+  type HydrationRenderer,
   MoveType,
   type Plugin,
   type RendererInternals,
@@ -11,6 +12,7 @@ import {
   type VaporInteropInterface,
   createVNode,
   currentInstance,
+  ensureHydrationRenderer,
   ensureRenderer,
   onScopeDispose,
   renderSlot,
@@ -33,11 +35,12 @@ import type { RawSlots, VaporSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
 import { createTextNode } from './dom/node'
 import { optimizePropertyLookup } from './dom/prop'
+import { hydrateNode as vaporHydrateNode } from './dom/hydration'
 
 // mounting vapor components and slots in vdom
 const vaporInteropImpl: Omit<
   VaporInteropInterface,
-  'vdomMount' | 'vdomUnmount' | 'vdomSlot'
+  'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomHydrate'
 > = {
   mount(vnode, container, anchor, parentComponent) {
     const selfAnchor = (vnode.el = vnode.anchor = createTextNode())
@@ -113,6 +116,8 @@ const vaporInteropImpl: Omit<
     insert(vnode.vb || (vnode.component as any), container, anchor)
     insert(vnode.anchor as any, container, anchor)
   },
+
+  hydrate: vaporHydrateNode,
 }
 
 const vaporSlotPropsProxyHandler: ProxyHandler<
@@ -147,7 +152,7 @@ function createVDOMComponent(
   component: ConcreteComponent,
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
-): VaporFragment {
+): [VaporFragment, VNode] {
   const frag = new VaporFragment([])
   const vnode = createVNode(
     component,
@@ -202,7 +207,7 @@ function createVDOMComponent(
 
   frag.remove = unmount
 
-  return frag
+  return [frag, vnode]
 }
 
 /**
@@ -279,11 +284,14 @@ function renderVDOMSlot(
 }
 
 export const vaporInteropPlugin: Plugin = app => {
-  const internals = ensureRenderer().internals
+  const { internals, hydrateNode } = (
+    app._ssr ? ensureHydrationRenderer() : ensureRenderer()
+  ) as HydrationRenderer
   app._context.vapor = extend(vaporInteropImpl, {
     vdomMount: createVDOMComponent.bind(null, internals),
     vdomUnmount: internals.umt,
     vdomSlot: renderVDOMSlot.bind(null, internals),
+    vdomHydrate: hydrateNode,
   })
   const mount = app.mount
   app.mount = ((...args) => {