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 { 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
}
/**

View 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

View 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 {

View 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
*/

View 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
}

View 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

View 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) => {