From 99d70ddd3146fdd307cdf57fd45194dd96f8a35a Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 6 Feb 2025 18:12:54 +0800 Subject: [PATCH] wip(vapor): vdom slots in vapor component --- packages/runtime-core/src/apiCreateApp.ts | 7 ++ .../runtime-core/src/helpers/renderSlot.ts | 9 +- packages/runtime-core/src/hmr.ts | 10 +- packages/runtime-core/src/renderer.ts | 16 +-- packages/runtime-vapor/src/component.ts | 5 +- packages/runtime-vapor/src/componentSlots.ts | 18 ++-- packages/runtime-vapor/src/vdomInterop.ts | 98 ++++++++++++++++--- 7 files changed, 131 insertions(+), 32 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 66dac629181..a76758d4841 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -194,6 +194,13 @@ export interface VaporInteropInterface { move(vnode: VNode, container: any, anchor: any): void vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any vdomUnmount: UnmountComponentFn + vdomSlot: ( + slots: any, + name: string | (() => string), + props: Record, + parentComponent: any, // VaporComponentInstance + fallback?: any, // VaporSlot + ) => any } /** diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 738c51aa6be..a043ab8d514 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -32,10 +32,11 @@ export function renderSlot( noSlotted?: boolean, ): VNode { if ( - currentRenderingInstance!.ce || - (currentRenderingInstance!.parent && - isAsyncWrapper(currentRenderingInstance!.parent) && - currentRenderingInstance!.parent.ce) + currentRenderingInstance && + (currentRenderingInstance.ce || + (currentRenderingInstance.parent && + isAsyncWrapper(currentRenderingInstance.parent) && + currentRenderingInstance.parent.ce)) ) { // in custom element mode, render as actual slot outlets // wrap it with a fragment because in shadowRoot: false mode the slot diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 6fd6bb3f2ea..ed5d8b081a0 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -7,7 +7,7 @@ import { type GenericComponentInstance, isClassComponent, } from './component' -import { queueJob, queuePostFlushCb } from './scheduler' +import { nextTick, queueJob, queuePostFlushCb } from './scheduler' import { extend, getGlobalThis } from '@vue/shared' type HMRComponent = ComponentOptions | ClassComponent @@ -102,7 +102,9 @@ function rerender(id: string, newRender?: Function): void { i.renderCache = [] i.update() } - isHmrUpdating = false + nextTick(() => { + isHmrUpdating = false + }) }) } @@ -160,7 +162,9 @@ function reload(id: string, newComp: HMRComponent): void { } else { ;(parent as ComponentInternalInstance).update() } - isHmrUpdating = false + nextTick(() => { + isHmrUpdating = false + }) // #6930, #11248 avoid infinite recursion dirtyInstances.delete(instance) }) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 60bc81f0c33..d711218886a 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -2530,15 +2530,17 @@ function resolveChildrenNamespace( } function toggleRecurse( - { effect, job }: ComponentInternalInstance, + { effect, job, vapor }: ComponentInternalInstance, allowed: boolean, ) { - if (allowed) { - effect.flags |= EffectFlags.ALLOW_RECURSE - job.flags! |= SchedulerJobFlags.ALLOW_RECURSE - } else { - effect.flags &= ~EffectFlags.ALLOW_RECURSE - job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE + if (!vapor) { + if (allowed) { + effect.flags |= EffectFlags.ALLOW_RECURSE + job.flags! |= SchedulerJobFlags.ALLOW_RECURSE + } else { + effect.flags &= ~EffectFlags.ALLOW_RECURSE + job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE + } } } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index b2207f7d48a..e46390f4610 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -313,10 +313,13 @@ export class VaporComponentInstance implements GenericComponentInstance { props: Record attrs: Record propsDefaults: Record | null - rawPropsRef?: ShallowRef // to hold vnode props in vdom interop mode slots: StaticSlots + // to hold vnode props / slots in vdom interop mode + rawPropsRef?: ShallowRef + rawSlotsRef?: ShallowRef + emit: EmitFn emitted: Record | null diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 02a6ebfbbe0..b67b6e98e80 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -85,10 +85,6 @@ export function getSlot( } } -// TODO how to handle empty slot return blocks? -// e.g. a slot renders a v-if node that may toggle inside. -// we may need special handling by passing the fallback into the slot -// and make the v-if use it as fallback export function createSlot( name: string | (() => string), rawProps?: LooseRawProps | null, @@ -96,12 +92,22 @@ export function createSlot( ): Block { const instance = currentInstance as VaporComponentInstance const rawSlots = instance.rawSlots - const isDynamicName = isFunction(name) - const fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment() const slotProps = rawProps ? new Proxy(rawProps, rawPropsProxyHandlers) : EMPTY_OBJ + if (rawSlots._) { + return instance.appContext.vapor!.vdomSlot( + rawSlots._, + name, + slotProps, + instance, + fallback, + ) + } + + const isDynamicName = isFunction(name) + const fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment() const renderSlot = () => { const slot = getSlot(rawSlots, isFunction(name) ? name() : name) if (slot) { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 7b9ee19cbfe..d258066d631 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -3,41 +3,57 @@ import { type ConcreteComponent, type Plugin, type RendererInternals, + type ShallowRef, + type Slots, + type VNode, type VaporInteropInterface, createVNode, currentInstance, ensureRenderer, + renderSlot, shallowRef, simpleSetCurrentInstance, } from '@vue/runtime-dom' import { type LooseRawProps, type LooseRawSlots, + type VaporComponent, VaporComponentInstance, createComponent, mountComponent, unmountComponent, } from './component' -import { VaporFragment, insert } from './block' -import { extend, remove } from '@vue/shared' +import { type Block, VaporFragment, insert, remove } from './block' +import { extend, isFunction, remove as removeItem } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' -import type { RawSlots } from './componentSlots' +import type { RawSlots, VaporSlot } from './componentSlots' +import { renderEffect } from './renderEffect' const vaporInteropImpl: Omit< VaporInteropInterface, - 'vdomMount' | 'vdomUnmount' + 'vdomMount' | 'vdomUnmount' | 'vdomSlot' > = { mount(vnode, container, anchor, parentComponent) { const selfAnchor = (vnode.anchor = document.createComment('vapor')) container.insertBefore(selfAnchor, anchor) const prev = currentInstance simpleSetCurrentInstance(parentComponent) + const propsRef = shallowRef(vnode.props) + const slotsRef = shallowRef(vnode.children) + // @ts-expect-error - const instance = (vnode.component = createComponent(vnode.type, { - $: [() => propsRef.value], - })) + const instance = (vnode.component = createComponent( + vnode.type as any as VaporComponent, + { + $: [() => propsRef.value], + } as RawProps, + { + _: slotsRef, // pass the slots ref + } as any as RawSlots, + )) instance.rawPropsRef = propsRef + instance.rawSlotsRef = slotsRef mountComponent(instance, container, selfAnchor) simpleSetCurrentInstance(prev) return instance @@ -46,8 +62,9 @@ const vaporInteropImpl: Omit< update(n1, n2, shouldUpdate) { n2.component = n1.component if (shouldUpdate) { - ;(n2.component as any as VaporComponentInstance).rawPropsRef!.value = - n2.props + const instance = n2.component as any as VaporComponentInstance + instance.rawPropsRef!.value = n2.props + instance.rawSlotsRef!.value = n2.children } }, @@ -109,8 +126,66 @@ function createVDOMComponent( } frag.remove = () => { internals.umt(vnode.component!, null, true) - remove(parentInstance.vdomChildren!, vnode.component) - isMounted = false + removeItem(parentInstance.vdomChildren!, vnode.component) + } + + return frag +} + +function renderVDOMSlot( + internals: RendererInternals, + slotsRef: ShallowRef, + name: string | (() => string), + props: Record, + parentComponent: VaporComponentInstance, + fallback?: VaporSlot, +): VaporFragment { + const frag = new VaporFragment([]) + + let isMounted = false + let fallbackNodes: Block | undefined + let parentNode: ParentNode + let oldVNode: VNode | null = null + + frag.insert = (parent, anchor) => { + parentNode = parent + if (!isMounted) { + renderEffect(() => { + const vnode = renderSlot( + slotsRef.value, + isFunction(name) ? name() : name, + props, + ) + if ((vnode.children as any[]).length) { + if (fallbackNodes) { + remove(fallbackNodes, parentNode) + fallbackNodes = undefined + } + internals.p(oldVNode, vnode, parent, anchor, parentComponent as any) + oldVNode = vnode + } else { + if (fallback && !fallbackNodes) { + // mount fallback + if (oldVNode) { + internals.um(oldVNode, parentComponent as any, null, true) + } + insert((fallbackNodes = fallback(props)), parent, anchor) + } + oldVNode = null + } + }) + isMounted = true + } else { + // TODO move + } + + frag.remove = () => { + if (fallbackNodes) { + remove(fallbackNodes, parentNode) + } else if (oldVNode) { + internals.um(oldVNode, parentComponent as any, null) + } + } } return frag @@ -121,5 +196,6 @@ export const vaporInteropPlugin: Plugin = app => { app._context.vapor = extend(vaporInteropImpl, { vdomMount: createVDOMComponent.bind(null, internals), vdomUnmount: internals.umt, + vdomSlot: renderVDOMSlot.bind(null, internals), }) }