wip: vdom interop

This commit is contained in:
daiwei 2025-04-28 11:06:04 +08:00
parent 700f49ee96
commit e5399c3418
7 changed files with 83 additions and 27 deletions

View File

@ -33,6 +33,7 @@ import type { NormalizedPropsOptions } from './componentProps'
import type { ObjectEmitsOptions } from './componentEmits' import type { ObjectEmitsOptions } from './componentEmits'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling' import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { DefineComponent } from './apiDefineComponent' import type { DefineComponent } from './apiDefineComponent'
import type { createHydrationFunctions } from './hydration'
export interface App<HostElement = any> { export interface App<HostElement = any> {
version: string version: string
@ -104,6 +105,7 @@ export interface App<HostElement = any> {
_container: HostElement | null _container: HostElement | null
_context: AppContext _context: AppContext
_instance: GenericComponentInstance | null _instance: GenericComponentInstance | null
_ssr?: boolean
/** /**
* @internal custom element vnode * @internal custom element vnode
@ -193,6 +195,7 @@ export interface VaporInteropInterface {
unmount(vnode: VNode, doRemove?: boolean): void unmount(vnode: VNode, doRemove?: boolean): void
move(vnode: VNode, container: any, anchor: any): void move(vnode: VNode, container: any, anchor: any): void
slot(n1: VNode | null, n2: 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 vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
vdomUnmount: UnmountComponentFn vdomUnmount: UnmountComponentFn
@ -203,6 +206,7 @@ export interface VaporInteropInterface {
parentComponent: any, // VaporComponentInstance parentComponent: any, // VaporComponentInstance
fallback?: any, // VaporSlot fallback?: any, // VaporSlot
) => any ) => any
vdomHydrate: ReturnType<typeof createHydrationFunctions>[1] | undefined
} }
/** /**

View File

@ -37,7 +37,11 @@ import {
normalizeStyle, normalizeStyle,
stringifyStyle, stringifyStyle,
} from '@vue/shared' } from '@vue/shared'
import { type RendererInternals, needTransition } from './renderer' import {
type RendererInternals,
getVaporInterface,
needTransition,
} from './renderer'
import { setRef } from './rendererTemplateRef' import { setRef } from './rendererTemplateRef'
import { import {
type SuspenseBoundary, type SuspenseBoundary,
@ -294,10 +298,6 @@ export function createHydrationFunctions(
) )
} }
} else if (shapeFlag & ShapeFlags.COMPONENT) { } 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 // when setting up the render effect, if the initial vnode already
// has .el set, the component will perform hydration instead of mount // has .el set, the component will perform hydration instead of mount
// on its sub-tree. // on its sub-tree.
@ -318,15 +318,23 @@ export function createHydrationFunctions(
nextNode = nextSibling(node) nextNode = nextSibling(node)
} }
mountComponent( // hydrate vapor component
vnode, if ((vnode.type as ConcreteComponent).__vapor) {
container, const vaporInterface = getVaporInterface(parentComponent, vnode)
null, vaporInterface.hydrate(node, () => {
parentComponent, vaporInterface.mount(vnode, container, null, parentComponent)
parentSuspense, })
getContainerType(container), } else {
optimized, mountComponent(
) vnode,
container,
null,
parentComponent,
parentSuspense,
getContainerType(container),
optimized,
)
}
// #3787 // #3787
// if component is async, it may get moved / unmounted before its // if component is async, it may get moved / unmounted before its

View File

@ -107,6 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> { export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
hydrate: RootHydrateFunction hydrate: RootHydrateFunction
hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
} }
export type ElementNamespace = 'svg' | 'mathml' | undefined export type ElementNamespace = 'svg' | 'mathml' | undefined
@ -2524,6 +2525,7 @@ function baseCreateRenderer(
return { return {
render, render,
hydrate, hydrate,
hydrateNode,
internals, internals,
createApp: createAppAPI( createApp: createAppAPI(
mountApp, mountApp,
@ -2639,7 +2641,10 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
} }
} }
function getVaporInterface( /**
* @internal
*/
export function getVaporInterface(
instance: ComponentInternalInstance | null, instance: ComponentInternalInstance | null,
vnode: VNode, vnode: VNode,
): VaporInteropInterface { ): VaporInteropInterface {

View File

@ -149,6 +149,7 @@ export const createApp = ((...args) => {
export const createSSRApp = ((...args) => { export const createSSRApp = ((...args) => {
const app = ensureHydrationRenderer().createApp(...args) const app = ensureHydrationRenderer().createApp(...args)
app._ssr = true
if (__DEV__) { if (__DEV__) {
injectNativeTagCheck(app) injectNativeTagCheck(app)
@ -319,7 +320,7 @@ export * from './jsx'
/** /**
* @internal * @internal
*/ */
export { ensureRenderer, normalizeContainer } export { ensureRenderer, ensureHydrationRenderer, normalizeContainer }
/** /**
* @internal * @internal
*/ */

View File

@ -58,7 +58,11 @@ import {
getSlot, getSlot,
} from './componentSlots' } from './componentSlots'
import { hmrReload, hmrRerender } from './hmr' import { hmrReload, hmrRerender } from './hmr'
import { isHydrating, locateHydrationNode } from './dom/hydration' import {
currentHydrationNode,
isHydrating,
locateHydrationNode,
} from './dom/hydration'
import { import {
insertionAnchor, insertionAnchor,
insertionParent, insertionParent,
@ -152,13 +156,22 @@ export function createComponent(
// vdom interop enabled and component is not an explicit vapor component // vdom interop enabled and component is not an explicit vapor component
if (appContext.vapor && !component.__vapor) { if (appContext.vapor && !component.__vapor) {
const frag = appContext.vapor.vdomMount( const [frag, vnode] = appContext.vapor.vdomMount(
component as any, component as any,
rawProps, rawProps,
rawSlots, rawSlots,
) )
if (!isHydrating && _insertionParent) { if (!isHydrating && _insertionParent) {
insert(frag, _insertionParent, _insertionAnchor) insert(frag, _insertionParent, _insertionAnchor)
} else if (isHydrating) {
appContext.vapor.vdomHydrate!(
currentHydrationNode!,
vnode,
currentInstance as any,
null,
null,
false,
)
} }
return frag return frag
} }

View File

@ -22,10 +22,15 @@ export function setCurrentHydrationNode(node: Node | null): void {
let isOptimized = false let isOptimized = false
export function withHydration(container: ParentNode, fn: () => void): void { function performHydration<T>(
adoptTemplate = adoptTemplateImpl fn: () => T,
locateHydrationNode = locateHydrationNodeImpl setup: () => void,
cleanup: () => void,
): T {
if (!isOptimized) { if (!isOptimized) {
adoptTemplate = adoptTemplateImpl
locateHydrationNode = locateHydrationNodeImpl
// optimize anchor cache lookup // optimize anchor cache lookup
;(Comment.prototype as any).$fs = undefined ;(Comment.prototype as any).$fs = undefined
;(Node.prototype as any).$nc = undefined ;(Node.prototype as any).$nc = undefined
@ -33,15 +38,27 @@ export function withHydration(container: ParentNode, fn: () => void): void {
} }
enableHydrationNodeLookup() enableHydrationNodeLookup()
isHydrating = true isHydrating = true
setInsertionState(container, 0) setup()
const res = fn() const res = fn()
resetInsertionState() cleanup()
currentHydrationNode = null currentHydrationNode = null
isHydrating = false isHydrating = false
disableHydrationNodeLookup() disableHydrationNodeLookup()
return res 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 adoptTemplate: (node: Node, template: string) => Node | null
export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void

View File

@ -2,6 +2,7 @@ import {
type App, type App,
type ComponentInternalInstance, type ComponentInternalInstance,
type ConcreteComponent, type ConcreteComponent,
type HydrationRenderer,
MoveType, MoveType,
type Plugin, type Plugin,
type RendererInternals, type RendererInternals,
@ -11,6 +12,7 @@ import {
type VaporInteropInterface, type VaporInteropInterface,
createVNode, createVNode,
currentInstance, currentInstance,
ensureHydrationRenderer,
ensureRenderer, ensureRenderer,
onScopeDispose, onScopeDispose,
renderSlot, renderSlot,
@ -33,11 +35,12 @@ import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node' import { createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop' import { optimizePropertyLookup } from './dom/prop'
import { hydrateNode as vaporHydrateNode } from './dom/hydration'
// mounting vapor components and slots in vdom // mounting vapor components and slots in vdom
const vaporInteropImpl: Omit< const vaporInteropImpl: Omit<
VaporInteropInterface, VaporInteropInterface,
'vdomMount' | 'vdomUnmount' | 'vdomSlot' 'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomHydrate'
> = { > = {
mount(vnode, container, anchor, parentComponent) { mount(vnode, container, anchor, parentComponent) {
const selfAnchor = (vnode.el = vnode.anchor = createTextNode()) 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.vb || (vnode.component as any), container, anchor)
insert(vnode.anchor as any, container, anchor) insert(vnode.anchor as any, container, anchor)
}, },
hydrate: vaporHydrateNode,
} }
const vaporSlotPropsProxyHandler: ProxyHandler< const vaporSlotPropsProxyHandler: ProxyHandler<
@ -147,7 +152,7 @@ function createVDOMComponent(
component: ConcreteComponent, component: ConcreteComponent,
rawProps?: LooseRawProps | null, rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null, rawSlots?: LooseRawSlots | null,
): VaporFragment { ): [VaporFragment, VNode] {
const frag = new VaporFragment([]) const frag = new VaporFragment([])
const vnode = createVNode( const vnode = createVNode(
component, component,
@ -202,7 +207,7 @@ function createVDOMComponent(
frag.remove = unmount frag.remove = unmount
return frag return [frag, vnode]
} }
/** /**
@ -279,11 +284,14 @@ function renderVDOMSlot(
} }
export const vaporInteropPlugin: Plugin = app => { export const vaporInteropPlugin: Plugin = app => {
const internals = ensureRenderer().internals const { internals, hydrateNode } = (
app._ssr ? ensureHydrationRenderer() : ensureRenderer()
) as HydrationRenderer
app._context.vapor = extend(vaporInteropImpl, { app._context.vapor = extend(vaporInteropImpl, {
vdomMount: createVDOMComponent.bind(null, internals), vdomMount: createVDOMComponent.bind(null, internals),
vdomUnmount: internals.umt, vdomUnmount: internals.umt,
vdomSlot: renderVDOMSlot.bind(null, internals), vdomSlot: renderVDOMSlot.bind(null, internals),
vdomHydrate: hydrateNode,
}) })
const mount = app.mount const mount = app.mount
app.mount = ((...args) => { app.mount = ((...args) => {