From: daiwei Date: Mon, 28 Apr 2025 03:06:04 +0000 (+0800) Subject: wip: vdom interop X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e5399c3418919ae47e57691c3b483531d2080ec5;p=thirdparty%2Fvuejs%2Fcore.git wip: vdom interop --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index d8ae73fb69..63690d0cea 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -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 { version: string @@ -104,6 +105,7 @@ export interface App { _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[1] | undefined } /** diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 6c702f6838..e1420d3353 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -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 diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5a18d62a8e..fade2a4b39 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -107,6 +107,7 @@ export interface Renderer { export interface HydrationRenderer extends Renderer { hydrate: RootHydrateFunction + hydrateNode: ReturnType[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 { diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 51c72fe2ed..14387e4775 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -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 */ diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 03675475b8..939555ea66 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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 } diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index 3ed3ee2b86..9b2e0efa74 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -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( + 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 diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a..6f3bc6c69d 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -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) => {