From: daiwei Date: Wed, 6 Aug 2025 09:38:55 +0000 (+0800) Subject: refactor: hydration vdom interop X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8eedd1e56c929fadb4dffa1d8a85608645446144;p=thirdparty%2Fvuejs%2Fcore.git refactor: hydration vdom interop --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 43c96e5003..0f379b4138 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -191,8 +191,8 @@ export interface VaporInteropInterface { component: ComponentInternalInstance, transition: TransitionHooks, ): void - hydrate(node: Node, fn: () => void): void - hydrateSlot(vnode: VNode, container: any): void + hydrate(node: Node, fn: () => void): Node + hydrateSlot(vnode: VNode, node: any): Node vdomMount: ( component: ConcreteComponent, diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 5db9a3bdd5..f7f15d2f3b 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -279,9 +279,9 @@ export function createHydrationFunctions( } break case VaporSlot: - getVaporInterface(parentComponent, vnode).hydrateSlot( + nextNode = getVaporInterface(parentComponent, vnode).hydrateSlot( vnode, - parentNode(node)!, + node, ) break default: @@ -327,7 +327,7 @@ export function createHydrationFunctions( // hydrate vapor component if ((vnode.type as ConcreteComponent).__vapor) { const vaporInterface = getVaporInterface(parentComponent, vnode) - vaporInterface.hydrate(node, () => { + nextNode = vaporInterface.hydrate(node, () => { vaporInterface.mount(vnode, container, null, parentComponent) }) } else { diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 1bf7767352..659f8487a0 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -49,11 +49,13 @@ export function createDynamicComponent( ) }) - if (!isHydrating && _insertionParent) { - insert(frag, _insertionParent, _insertionAnchor) - } - if (isHydrating && _insertionAnchor !== undefined) { - advanceHydrationNode(_insertionParent!) + if (!isHydrating) { + if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor) + } else { + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } + return frag } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 249d03d717..09a3d2fc51 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -471,10 +471,9 @@ export const createFor = ( renderEffect(renderList) } - if (!isHydrating && _insertionParent) { - insert(frag, _insertionParent, _insertionAnchor) - } - if (isHydrating) { + if (!isHydrating) { + if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor) + } else { advanceHydrationNode( _insertionAnchor !== undefined ? _insertionParent! : parentAnchor!, ) diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index 4ed055554d..ac9f1ffd9a 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -74,15 +74,15 @@ export function createIf( isHydrating && ifStack.pop() } - if (!isHydrating && _insertionParent) { - insert(frag, _insertionParent, _insertionAnchor) - } - - // if _insertionAnchor is defined, insertionParent contains a static node - // that should be skipped during hydration. - // Advance to the next sibling node to bypass this static node. - if (isHydrating && _insertionAnchor !== undefined) { - advanceHydrationNode(_insertionParent!) + if (!isHydrating) { + if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor) + } else { + // if _insertionAnchor is defined, insertionParent contains a static node + // that should be skipped during hydration. + // Advance to the next sibling node to bypass this static node. + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } return frag diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index f9bac4347f..a6bc4869aa 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -179,27 +179,28 @@ export function createComponent( scopeId, ) - // `frag.insert` handles both hydration and mounting - if (_insertionParent) { - insert(frag, _insertionParent, _insertionAnchor) - } - if (isHydrating && _insertionAnchor !== undefined) { - advanceHydrationNode(_insertionParent!) + if (!isHydrating) { + if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor) + } else { + frag.hydrate() + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } + return frag } // teleport if (isVaporTeleport(component)) { const frag = component.process(rawProps!, rawSlots!) - if (!isHydrating && _insertionParent) { - insert(frag, _insertionParent, _insertionAnchor) + if (!isHydrating) { + if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor) } else { frag.hydrate() - } - - if (isHydrating && _insertionAnchor !== undefined) { - advanceHydrationNode(_insertionParent!) + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } return frag as any @@ -316,8 +317,14 @@ export function createComponent( if (scopeId) setScopeId(instance.block, scopeId) - if (!isHydrating && _insertionParent) { - mountComponent(instance, _insertionParent, _insertionAnchor) + if (!isHydrating) { + if (_insertionParent) { + mountComponent(instance, _insertionParent, _insertionAnchor) + } + } else { + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } return instance } @@ -591,12 +598,12 @@ export function createComponentWithFallback( } } - if (!isHydrating && _insertionParent) { - insert(el, _insertionParent, _insertionAnchor) - } - - if (isHydrating && _insertionAnchor !== undefined) { - advanceHydrationNode(_insertionParent!) + if (!isHydrating) { + if (_insertionParent) insert(el, _insertionParent, _insertionAnchor) + } else { + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } return el diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 3b4adeed9f..1e8b30128e 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -16,8 +16,12 @@ import { insertionParent, resetInsertionState, } from './insertionState' -import { advanceHydrationNode, isHydrating } from './dom/hydration' -import { DynamicFragment } from './fragment' +import { + advanceHydrationNode, + isHydrating, + locateHydrationNode, +} from './dom/hydration' +import { DynamicFragment, type VaporFragment } from './fragment' export type RawSlots = Record & { $?: DynamicSlotSource[] @@ -127,6 +131,7 @@ export function createSlot( let fragment: DynamicFragment if (isRef(rawSlots._)) { + if (isHydrating) locateHydrationNode() fragment = instance.appContext.vapor!.vdomSlot( rawSlots._, name, @@ -166,17 +171,15 @@ export function createSlot( if (scopeId) setScopeId(fragment, `${scopeId}-s`) } - if ( - _insertionParent && - (!isHydrating || - // for vdom interop fragment, `fragment.insert` handles both hydration and mounting - fragment.insert) - ) { - insert(fragment, _insertionParent, _insertionAnchor) - } - - if (isHydrating && _insertionAnchor !== undefined) { - advanceHydrationNode(_insertionParent!) + if (!isHydrating) { + if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor) + } else { + if (fragment.insert) { + ;(fragment as VaporFragment).hydrate!() + } + if (_insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } } return fragment diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index dc4dab68cd..d80399373e 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -209,7 +209,7 @@ export class TeleportFragment extends VaporFragment { this.mountAnchor = undefined } - hydrate(): void { + hydrate = (): void => { // TODO } } diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 53d83adf3d..9cf99e38e0 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -36,6 +36,7 @@ export class VaporFragment anchor: Node | null, transitionHooks?: TransitionHooks, ) => void + hydrate?: (...args: any[]) => any remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void fallback?: BlockFn @@ -155,7 +156,7 @@ export class DynamicFragment extends VaporFragment { } } - hydrate(label: string, isEmpty: boolean = false): void { + hydrate = (label: string, isEmpty: boolean = false): void => { // for `v-if="false"`, the node will be an empty comment, use it as the anchor. // otherwise, find next sibling vapor fragment anchor if (label === 'if' && isEmpty) { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index d16c73127d..af8bef7406 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, + Fragment, type HydrationRenderer, MoveType, type Plugin, @@ -20,6 +21,7 @@ import { ensureRenderer, ensureVaporSlotFallback, isEmitListener, + isRef, isVNode, onScopeDispose, renderSlot, @@ -34,6 +36,7 @@ import { type VaporComponent, VaporComponentInstance, createComponent, + isVaporComponent, mountComponent, unmountComponent, } from './component' @@ -54,9 +57,11 @@ import { setTransitionHooks as setVaporTransitionHooks } from './components/Tran import { advanceHydrationNode, currentHydrationNode, + isComment, isHydrating, locateHydrationNode, locateVaporFragmentAnchor, + setCurrentHydrationNode, hydrateNode as vaporHydrateNode, } from './dom/hydration' import { VaporFragment, isFragment, setFragmentFallback } from './fragment' @@ -99,6 +104,10 @@ const vaporInteropImpl: Omit< { _: slotsRef, // pass the slots ref } as any as RawSlots, + undefined, + undefined, + undefined, + (parentComponent ? parentComponent.appContext : vnode.appContext) as any, )) instance.rawPropsRef = propsRef instance.rawSlotsRef = slotsRef @@ -169,16 +178,21 @@ const vaporInteropImpl: Omit< setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks) }, - hydrate: vaporHydrateNode, - hydrateSlot(vnode, container) { + hydrate(node, fn) { + vaporHydrateNode(node, fn) + return __next(node) + }, + hydrateSlot(vnode, node) { const { slot } = vnode.vs! const propsRef = (vnode.vs!.ref = shallowRef(vnode.props)) - const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler)) - vaporHydrateNode(slotBlock, () => { - const anchor = locateVaporFragmentAnchor(currentHydrationNode!, 'slot')! - vnode.el = vnode.anchor = anchor - insert((vnode.vb = slotBlock), container, anchor) + vaporHydrateNode(node, () => { + vnode.vb = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler)) + vnode.el = vnode.anchor = locateVaporFragmentAnchor( + currentHydrationNode!, + 'slot', + ) }) + return __next(node) }, } @@ -256,25 +270,29 @@ function createVDOMComponent( vnode.scopeId = parentInstance && parentInstance.type.__scopeId! + frag.hydrate = () => { + hydrateVNode(vnode, parentInstance as any) + onScopeDispose(unmount, true) + isMounted = true + frag.nodes = [vnode.el as Node] + } + frag.insert = (parentNode, anchor, transition) => { + if (isHydrating) return + const prev = currentInstance simpleSetCurrentInstance(parentInstance) - if (!isMounted || isHydrating) { + if (!isMounted) { if (transition) setVNodeTransitionHooks(vnode, transition) - - if (isHydrating) { - hydrateVNode(vnode, parentInstance as any) - } else { - internals.mt( - vnode, - parentNode, - anchor, - parentInstance as any, - null, - undefined, - false, - ) - } + internals.mt( + vnode, + parentNode, + anchor, + parentInstance as any, + null, + undefined, + false, + ) onScopeDispose(unmount, true) isMounted = true } else { @@ -317,76 +335,14 @@ function renderVDOMSlot( frag.fallback = fallback frag.insert = (parentNode, anchor) => { - if (!isMounted) { - renderEffect(() => { - let vnode: VNode | undefined - let isValidSlot = false - // only render slot if rawSlots is defined and slot nodes are not empty - // otherwise, render fallback - if (slotsRef.value) { - vnode = renderSlot( - slotsRef.value, - isFunction(name) ? name() : name, - props, - ) + if (isHydrating) return - let children = vnode.children as any[] - // handle forwarded vapor slot without its own fallback - // use the fallback provided by the slot outlet - ensureVaporSlotFallback(children, fallback as any) - isValidSlot = children.length > 0 - } - if (isValidSlot) { - if (isHydrating) { - hydrateVNode(vnode!, parentComponent as any) - } else { - if (fallbackNodes) { - remove(fallbackNodes, parentNode) - fallbackNodes = undefined - } - internals.p( - oldVNode, - vnode!, - parentNode, - anchor, - parentComponent as any, - null, - undefined, - null, - false, - ) - } - oldVNode = vnode! - } else { - // for forwarded slot without its own fallback, use the fallback - // provided by the slot outlet. - // re-fetch `frag.fallback` as it may have been updated at `createSlot` - fallback = frag.fallback - if (fallback && !fallbackNodes) { - // mount fallback - if (oldVNode) { - internals.um(oldVNode, parentComponent as any, null, true) - } - insert( - (fallbackNodes = fallback(internals, parentComponent)), - parentNode, - anchor, - ) - } else if (isHydrating) { - // update hydration node to the next sibling of the slot anchor - const nextNode = locateVaporFragmentAnchor( - currentHydrationNode!, - 'slot', - ) - if (nextNode) advanceHydrationNode(nextNode) - } - oldVNode = null - } - }) + if (!isMounted) { + render(parentNode, anchor) isMounted = true } else { // move - if (oldVNode && !isHydrating) { + if (oldVNode) { internals.m( oldVNode, parentNode, @@ -406,6 +362,72 @@ function renderVDOMSlot( } } + const render = (parentNode?: ParentNode, anchor?: Node | null) => { + renderEffect(() => { + let vnode: VNode | undefined + let isValidSlot = false + if (slotsRef.value) { + vnode = renderSlot( + slotsRef.value, + isFunction(name) ? name() : name, + props, + ) + + let children = vnode.children as any[] + // handle forwarded vapor slot without its own fallback + // use the fallback provided by the slot outlet + ensureVaporSlotFallback(children, fallback as any) + isValidSlot = children.length > 0 + } + if (isValidSlot) { + if (isHydrating) { + hydrateVNode(vnode!, parentComponent as any) + } else { + if (fallbackNodes) { + remove(fallbackNodes, parentNode) + fallbackNodes = undefined + } + internals.p( + oldVNode, + vnode!, + parentNode!, + anchor, + parentComponent as any, + null, + undefined, + null, + false, + ) + } + oldVNode = vnode! + } else { + // for forwarded slot without its own fallback, use the fallback + // provided by the slot outlet. + // re-fetch `frag.fallback` as it may have been updated at `createSlot` + fallback = frag.fallback + if (fallback && !fallbackNodes) { + fallbackNodes = fallback(internals, parentComponent) + if (isHydrating) { + // hydrate fallback + hydrateVNode(fallbackNodes as any, parentComponent as any) + } else { + // mount fallback + if (oldVNode) { + internals.um(oldVNode, parentComponent as any, null, true) + } + insert(fallbackNodes, parentNode!, anchor) + } + } + oldVNode = null + } + }) + } + + frag.hydrate = () => { + render() + isMounted = true + } + return frag } @@ -460,9 +482,24 @@ function hydrateVNode( vnode: VNode, parentComponent: ComponentInternalInstance | null, ) { - // keep fragment start anchor, hydrateNode uses it to - // determine if node is a fragmentStart locateHydrationNode() + + // skip fragment start anchor + let node = currentHydrationNode! + while ( + isComment(node, '[') && + // vnode is not a fragment + vnode.type !== Fragment && + // not inside vdom slot + !( + isVaporComponent(parentComponent) && + isRef((parentComponent as VaporComponentInstance).rawSlots._) + ) + ) { + node = node.nextSibling! + } + if (currentHydrationNode !== node) setCurrentHydrationNode(node) + if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode! const nextNode = vdomHydrateNode( currentHydrationNode!,