From: Evan You Date: Fri, 7 Feb 2025 07:47:06 +0000 (+0800) Subject: wip(vapor): vapor slots in vdom X-Git-Tag: v3.6.0-alpha.1~16^2~86 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=23939d09c68b52e9baf8f9126f951d2b14811304;p=thirdparty%2Fvuejs%2Fcore.git wip(vapor): vapor slots in vdom --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index a76758d484..30042368af 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -192,6 +192,8 @@ export interface VaporInteropInterface { update(n1: VNode, n2: VNode, shouldUpdate: boolean): void 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 + vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any vdomUnmount: UnmountComponentFn vdomSlot: ( diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 005e5dd3ee..f6ff8803c8 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -837,8 +837,8 @@ export function setupComponent( vi(instance) } else { initProps(instance, props, isStateful, isSSR) + initSlots(instance, children, optimized) } - initSlots(instance, children, optimized) const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index a043ab8d51..152c5a4b81 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -8,6 +8,7 @@ import { Fragment, type VNode, type VNodeArrayChildren, + VaporSlot, createBlock, createVNode, isVNode, @@ -31,6 +32,15 @@ export function renderSlot( fallback?: () => VNodeArrayChildren, noSlotted?: boolean, ): VNode { + let slot = slots[name] + + // vapor slots rendered in vdom + if (slot && slots._vapor) { + const ret = (openBlock(), createBlock(VaporSlot, props)) + ret.vs = { slot, fallback } + return ret + } + if ( currentRenderingInstance && (currentRenderingInstance.ce || @@ -53,8 +63,6 @@ export function renderSlot( ) } - let slot = slots[name] - if (__DEV__ && slot && slot.length > 1) { warn( `SSR-optimized slot function detected in a non-SSR-optimized render ` + diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index dee3010262..c7150e38e8 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -505,7 +505,7 @@ export { type VaporInteropInterface } from './apiCreateApp' /** * @internal */ -export { type RendererInternals } from './renderer' +export { type RendererInternals, MoveType } from './renderer' /** * @internal */ diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d711218886..fcbfdd0426 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -7,6 +7,7 @@ import { type VNodeArrayChildren, type VNodeHook, type VNodeProps, + VaporSlot, cloneIfMounted, cloneVNode, createVNode, @@ -445,6 +446,9 @@ function baseCreateRenderer( optimized, ) break + case VaporSlot: + getVaporInterface(parentComponent, n2).slot(n1, n2, container, anchor) + break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( @@ -2186,6 +2190,7 @@ function baseCreateRenderer( if (shapeFlag & ShapeFlags.COMPONENT) { if ((type as ConcreteComponent).__vapor) { getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove) + return } else { unmountComponent(vnode.component!, parentSuspense, doRemove) } @@ -2236,6 +2241,11 @@ function baseCreateRenderer( unmountChildren(children as VNode[], parentComponent, parentSuspense) } + if (type === VaporSlot) { + getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove) + return + } + if (doRemove) { remove(vnode) } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 2d476bd4af..4b31151da2 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -25,6 +25,7 @@ import type { RawSlots } from './componentSlots' import { type ReactiveFlags, type Ref, + type ShallowRef, isProxy, isRef, toRaw, @@ -70,6 +71,7 @@ export const Fragment = Symbol.for('v-fgt') as any as { export const Text: unique symbol = Symbol.for('v-txt') export const Comment: unique symbol = Symbol.for('v-cmt') export const Static: unique symbol = Symbol.for('v-stc') +export const VaporSlot: unique symbol = Symbol.for('v-vps') export type VNodeTypes = | string @@ -83,6 +85,7 @@ export type VNodeTypes = | typeof TeleportImpl | typeof Suspense | typeof SuspenseImpl + | typeof VaporSlot export type VNodeRef = | string @@ -258,6 +261,18 @@ export interface VNode< * @internal VDOM in Vapor interop hook */ vi?: (instance: ComponentInternalInstance) => void + /** + * @internal Vapor slot in VDOM metadata + */ + vs?: { + slot: (props: any) => any + fallback: (() => VNodeArrayChildren) | undefined + ref?: ShallowRef + } + /** + * @internal Vapor slot Block + */ + vb?: any } // Since v-if and v-for are the two possible ways node structure can dynamically diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b33e33a029..0b8bdee028 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -5,7 +5,7 @@ import { mountComponent, unmountComponent, } from './component' -import { createComment } from './dom/node' +import { createComment, createTextNode } from './dom/node' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' export type Block = @@ -37,9 +37,7 @@ export class DynamicFragment extends VaporFragment { constructor(anchorLabel?: string) { super([]) this.anchor = - __DEV__ && anchorLabel - ? createComment(anchorLabel) - : document.createTextNode('') + __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() } update(render?: BlockFn, key: any = render): void { diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index b67b6e98e8..9f6c2ba5a0 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,7 +1,7 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' import { type Block, type BlockFn, DynamicFragment } from './block' import { rawPropsProxyHandlers } from './componentProps' -import { currentInstance } from '@vue/runtime-core' +import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' @@ -96,7 +96,7 @@ export function createSlot( ? new Proxy(rawProps, rawPropsProxyHandlers) : EMPTY_OBJ - if (rawSlots._) { + if (isRef(rawSlots._)) { return instance.appContext.vapor!.vdomSlot( rawSlots._, name, diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index eb2d937735..741f385861 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -3,7 +3,7 @@ import { popWarningContext, pushWarningContext, simpleSetCurrentInstance, -} from '@vue/runtime-core' +} from '@vue/runtime-dom' import { insert, normalizeBlock, remove } from './block' import { type VaporComponent, diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 0e4556fe1e..a9fa9b3356 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -14,8 +14,8 @@ import { invokeArrayFns } from '@vue/shared' export function renderEffect(fn: () => void, noLifecycle = false): void { const instance = currentInstance as VaporComponentInstance | null const scope = getCurrentScope() - if (__DEV__ && !__TEST__ && !isVaporComponent(instance)) { - warn('renderEffect called without active vapor instance.') + if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) { + warn('renderEffect called without active EffectScope or Vapor instance.') } // renderEffect is always called after user has registered all hooks diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 76fecff67d..68cb00956f 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -1,6 +1,7 @@ import { type ComponentInternalInstance, type ConcreteComponent, + MoveType, type Plugin, type RendererInternals, type ShallowRef, @@ -10,6 +11,7 @@ import { createVNode, currentInstance, ensureRenderer, + onScopeDispose, renderSlot, shallowRef, simpleSetCurrentInstance, @@ -24,17 +26,19 @@ import { unmountComponent, } from './component' import { type Block, VaporFragment, insert, remove } from './block' -import { extend, isFunction } from '@vue/shared' +import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' +import { createTextNode } from './dom/node' +// mounting vapor components and slots in vdom const vaporInteropImpl: Omit< VaporInteropInterface, 'vdomMount' | 'vdomUnmount' | 'vdomSlot' > = { mount(vnode, container, anchor, parentComponent) { - const selfAnchor = (vnode.anchor = document.createComment('vapor')) + const selfAnchor = (vnode.el = vnode.anchor = createTextNode()) container.insertBefore(selfAnchor, anchor) const prev = currentInstance simpleSetCurrentInstance(parentComponent) @@ -61,6 +65,7 @@ const vaporInteropImpl: Omit< update(n1, n2, shouldUpdate) { n2.component = n1.component + n2.el = n2.anchor = n1.anchor if (shouldUpdate) { const instance = n2.component as any as VaporComponentInstance instance.rawPropsRef!.value = n2.props @@ -70,15 +75,71 @@ const vaporInteropImpl: Omit< unmount(vnode, doRemove) { const container = doRemove ? vnode.anchor!.parentNode : undefined - unmountComponent(vnode.component as any, container) + if (vnode.component) { + unmountComponent(vnode.component as any, container) + } else if (vnode.vb) { + remove(vnode.vb, container) + } + remove(vnode.anchor as Node, container) + }, + + /** + * vapor slot in vdom + */ + slot(n1: VNode, n2: VNode, container, anchor) { + if (!n1) { + // mount + const selfAnchor = (n2.el = n2.anchor = createTextNode()) + insert(selfAnchor, container, anchor) + const { slot, fallback } = n2.vs! + const propsRef = (n2.vs!.ref = shallowRef(n2.props)) + const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler)) + // TODO fallback for slot with v-if content + // fallback is a vnode slot function here, and slotBlock, if a DynamicFragment, + // expects a Vapor BlockFn as fallback + fallback + insert((n2.vb = slotBlock), container, selfAnchor) + } else { + // update + n2.el = n2.anchor = n1.anchor + n2.vb = n1.vb + ;(n2.vs!.ref = n1.vs!.ref)!.value = n2.props + } }, move(vnode, container, anchor) { - insert(vnode.component as any, container, anchor) + insert(vnode.vb || (vnode.component as any), container, anchor) insert(vnode.anchor as any, container, anchor) }, } +const vaporSlotPropsProxyHandler: ProxyHandler< + ShallowRef> +> = { + get(target, key: any) { + return target.value[key] + }, + has(target, key: any) { + return target.value[key] + }, + ownKeys(target) { + return Object.keys(target.value) + }, +} + +const vaporSlotsProxyHandler: ProxyHandler = { + get(target, key) { + if (key === '_vapor') { + return target + } else { + return target[key] + } + }, +} + +/** + * Mount vdom component in vapor + */ function createVDOMComponent( internals: RendererInternals, component: ConcreteComponent, @@ -100,35 +161,51 @@ function createVDOMComponent( vnode.vi = (instance: ComponentInternalInstance) => { instance.props = wrapper.props instance.attrs = wrapper.attrs - // TODO slots + instance.slots = + wrapper.slots === EMPTY_OBJ + ? EMPTY_OBJ + : new Proxy(wrapper.slots, vaporSlotsProxyHandler) } let isMounted = false const parentInstance = currentInstance as VaporComponentInstance - frag.insert = (parent, anchor) => { + const unmount = (parentNode?: ParentNode) => { + internals.umt(vnode.component!, null, !!parentNode) + } + + frag.insert = (parentNode, anchor) => { if (!isMounted) { internals.mt( vnode, - parent, + parentNode, anchor, parentInstance as any, null, undefined, false, ) - // TODO register unmount with onScopeDispose + onScopeDispose(unmount, true) isMounted = true } else { - // TODO move + // move + internals.m( + vnode, + parentNode, + anchor, + MoveType.REORDER, + parentInstance as any, + ) } } - frag.remove = parentNode => { - internals.umt(vnode.component!, null, !!parentNode) - } + + frag.remove = unmount return frag } +/** + * Mount vdom slot in vapor + */ function renderVDOMSlot( internals: RendererInternals, slotsRef: ShallowRef, @@ -156,7 +233,13 @@ function renderVDOMSlot( remove(fallbackNodes, parentNode) fallbackNodes = undefined } - internals.p(oldVNode, vnode, parent, anchor, parentComponent as any) + internals.p( + oldVNode, + vnode, + parentNode, + anchor, + parentComponent as any, + ) oldVNode = vnode } else { if (fallback && !fallbackNodes) { @@ -171,7 +254,14 @@ function renderVDOMSlot( }) isMounted = true } else { - // TODO move + // move + internals.m( + oldVNode!, + parentNode, + anchor, + MoveType.REORDER, + parentComponent as any, + ) } frag.remove = parentNode => {