From: Evan You Date: Sun, 2 Feb 2025 14:28:35 +0000 (+0800) Subject: wip(vapor): vapor in vdom interop X-Git-Tag: v3.6.0-alpha.1~16^2~105 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3464620f1226959b69ab198057e4a57011dded54;p=thirdparty%2Fvuejs%2Fcore.git wip(vapor): vapor in vdom interop --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 9162b0e004..8083e9d198 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -1,5 +1,6 @@ import { type Component, + type ComponentInternalInstance, type ConcreteComponent, type Data, type GenericComponent, @@ -171,6 +172,26 @@ export interface AppConfig extends GenericAppConfig { * @deprecated use config.compilerOptions.isCustomElement */ isCustomElement?: (tag: string) => boolean + + /** + * @internal + */ + vapor?: VaporInVDOMInterface +} + +/** + * @internal + */ +export interface VaporInVDOMInterface { + mount( + vnode: VNode, + container: any, + anchor: any, + parentComponent: ComponentInternalInstance | null, + ): GenericComponentInstance // VaporComponentInstance + update(n1: VNode, n2: VNode): void + unmount(vnode: VNode, doRemove?: boolean): void + move(vnode: VNode, container: any, anchor: any): void } /** diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 493d08b5a0..a34a72be05 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -192,6 +192,10 @@ export interface AllowedComponentProps { // Note: can't mark this whole interface internal because some public interfaces // extend it. export interface ComponentInternalOptions { + /** + * indicates vapor component + */ + __vapor?: boolean /** * @internal */ diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index b0fb27207a..2ceaaa9e60 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -90,13 +90,13 @@ const KeepAliveImpl: ComponentOptions = { }, setup(props: KeepAliveProps, { slots }: SetupContext) { - const instance = getCurrentInstance()! + const keepAliveInstance = getCurrentInstance()! // KeepAlive communicates with the instantiated renderer via the // ctx where the renderer passes in its internals, // and the KeepAlive instance exposes activate/deactivate implementations. // The whole point of this is to avoid importing KeepAlive directly in the // renderer to facilitate tree-shaking. - const sharedContext = instance.ctx as KeepAliveContext + const sharedContext = keepAliveInstance.ctx as KeepAliveContext // if the internal renderer is not registered, it indicates that this is server-side rendering, // for KeepAlive, we just need to render its children @@ -112,10 +112,10 @@ const KeepAliveImpl: ComponentOptions = { let current: VNode | null = null if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { - ;(instance as any).__v_cache = cache + ;(keepAliveInstance as any).__v_cache = cache } - const parentSuspense = instance.suspense + const parentSuspense = keepAliveInstance.suspense const { renderer: { @@ -135,7 +135,14 @@ const KeepAliveImpl: ComponentOptions = { optimized, ) => { const instance = vnode.component! - move(vnode, container, anchor, MoveType.ENTER, parentSuspense) + move( + vnode, + container, + anchor, + MoveType.ENTER, + keepAliveInstance, + parentSuspense, + ) // in case props have changed patch( instance.vnode, @@ -170,7 +177,14 @@ const KeepAliveImpl: ComponentOptions = { invalidateMount(instance.m) invalidateMount(instance.a) - move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) + move( + vnode, + storageContainer, + null, + MoveType.LEAVE, + keepAliveInstance, + parentSuspense, + ) queuePostRenderEffect(() => { if (instance.da) { invokeArrayFns(instance.da) @@ -191,7 +205,7 @@ const KeepAliveImpl: ComponentOptions = { function unmount(vnode: VNode) { // reset the shapeFlag so it can be properly unmounted resetShapeFlag(vnode) - _unmount(vnode, instance, parentSuspense, true) + _unmount(vnode, keepAliveInstance, parentSuspense, true) } function pruneCache(filter: (name: string) => boolean) { @@ -234,12 +248,15 @@ const KeepAliveImpl: ComponentOptions = { if (pendingCacheKey != null) { // if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves // avoid caching vnode that not been mounted - if (isSuspense(instance.subTree.type)) { + if (isSuspense(keepAliveInstance.subTree.type)) { queuePostRenderEffect(() => { - cache.set(pendingCacheKey!, getInnerChild(instance.subTree)) - }, instance.subTree.suspense) + cache.set( + pendingCacheKey!, + getInnerChild(keepAliveInstance.subTree), + ) + }, keepAliveInstance.subTree.suspense) } else { - cache.set(pendingCacheKey, getInnerChild(instance.subTree)) + cache.set(pendingCacheKey, getInnerChild(keepAliveInstance.subTree)) } } } @@ -248,7 +265,7 @@ const KeepAliveImpl: ComponentOptions = { onBeforeUnmount(() => { cache.forEach(cached => { - const { subTree, suspense } = instance + const { subTree, suspense } = keepAliveInstance const vnode = getInnerChild(subTree) if (cached.type === vnode.type && cached.key === vnode.key) { // current instance will be unmounted as part of keep-alive's unmount diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 85001f500c..0f6f69c652 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -549,6 +549,7 @@ function createSuspenseBoundary( container, anchor === initialAnchor ? next(activeBranch!) : anchor, MoveType.ENTER, + parentComponent, ) queuePostFlushCb(effects) } @@ -573,7 +574,13 @@ function createSuspenseBoundary( } if (!delayEnter) { // move content from off-dom container to actual container - move(pendingBranch!, container, anchor, MoveType.ENTER) + move( + pendingBranch!, + container, + anchor, + MoveType.ENTER, + parentComponent, + ) } } @@ -672,7 +679,7 @@ function createSuspenseBoundary( move(container, anchor, type) { suspense.activeBranch && - move(suspense.activeBranch, container, anchor, type) + move(suspense.activeBranch, container, anchor, type, parentComponent) suspense.container = container }, diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index fe6fa36c1c..a6445df7b0 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -244,6 +244,7 @@ export const TeleportImpl = { container, mainAnchor, internals, + parentComponent, TeleportMoveTypes.TOGGLE, ) } else { @@ -267,6 +268,7 @@ export const TeleportImpl = { nextTarget, null, internals, + parentComponent, TeleportMoveTypes.TARGET_CHANGE, ) } else if (__DEV__) { @@ -284,6 +286,7 @@ export const TeleportImpl = { target, targetAnchor, internals, + parentComponent, TeleportMoveTypes.TOGGLE, ) } @@ -346,6 +349,7 @@ function moveTeleport( container: RendererElement, parentAnchor: RendererNode | null, { o: { insert }, m: move }: RendererInternals, + parentComponent: ComponentInternalInstance | null, moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER, ): void { // move target anchor if this is a target change. @@ -370,6 +374,7 @@ function moveTeleport( container, parentAnchor, MoveType.REORDER, + parentComponent, ) } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index a4629dde8d..7679c8b870 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -526,6 +526,7 @@ export { createAppAPI, type AppMountFn, type AppUnmountFn, + type VaporInVDOMInterface, } from './apiCreateApp' /** * @internal diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index e825bef1ce..182d8a99a7 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -17,6 +17,7 @@ import { import { type ComponentInternalInstance, type ComponentOptions, + type ConcreteComponent, type Data, type LifecycleHook, createComponentInstance, @@ -64,6 +65,7 @@ import { type AppMountFn, type AppUnmountFn, type CreateAppFunction, + type VaporInVDOMInterface, createAppAPI, } from './apiCreateApp' import { setRef } from './rendererTemplateRef' @@ -234,6 +236,7 @@ type MoveFn = ( container: RendererElement, anchor: RendererNode | null, type: MoveType, + parentComponent: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, ) => void @@ -1145,7 +1148,19 @@ function baseCreateRenderer( optimized: boolean, ) => { n2.slotScopeIds = slotScopeIds - if (n1 == null) { + + if ((n2.type as ConcreteComponent).__vapor) { + if (n1 == null) { + getVaporInterface(parentComponent).mount( + n2, + container, + anchor, + parentComponent, + ) + } else { + getVaporInterface(parentComponent).update(n1, n2) + } + } else if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( n2, @@ -2000,7 +2015,13 @@ function baseCreateRenderer( // There is no stable subsequence (e.g. a reverse) // OR current node is not among the stable sequence if (j < 0 || i !== increasingNewIndexSequence[j]) { - move(nextChild, container, anchor, MoveType.REORDER) + move( + nextChild, + container, + anchor, + MoveType.REORDER, + parentComponent, + ) } else { j-- } @@ -2014,11 +2035,22 @@ function baseCreateRenderer( container, anchor, moveType, + parentComponent, parentSuspense = null, ) => { const { el, type, transition, children, shapeFlag } = vnode if (shapeFlag & ShapeFlags.COMPONENT) { - move(vnode.component!.subTree, container, anchor, moveType) + if ((type as ConcreteComponent).__vapor) { + getVaporInterface(parentComponent).move(vnode, container, anchor) + } else { + move( + vnode.component!.subTree, + container, + anchor, + moveType, + parentComponent, + ) + } return } @@ -2028,14 +2060,26 @@ function baseCreateRenderer( } if (shapeFlag & ShapeFlags.TELEPORT) { - ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals) + ;(type as typeof TeleportImpl).move( + vnode, + container, + anchor, + internals, + parentComponent, + ) return } if (type === Fragment) { hostInsert(el!, container, anchor) for (let i = 0; i < (children as VNode[]).length; i++) { - move((children as VNode[])[i], container, anchor, moveType) + move( + (children as VNode[])[i], + container, + anchor, + moveType, + parentComponent, + ) } hostInsert(vnode.anchor!, container, anchor) return @@ -2126,7 +2170,11 @@ function baseCreateRenderer( } if (shapeFlag & ShapeFlags.COMPONENT) { - unmountComponent(vnode.component!, parentSuspense, doRemove) + if ((type as ConcreteComponent).__vapor) { + getVaporInterface(parentComponent).unmount(vnode, doRemove) + } else { + unmountComponent(vnode.component!, parentSuspense, doRemove) + } } else { if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { vnode.suspense!.unmount(parentSuspense, doRemove) @@ -2553,3 +2601,19 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void { hooks[i].flags! |= SchedulerJobFlags.DISPOSED } } + +function getVaporInterface( + instance: ComponentInternalInstance | null, +): VaporInVDOMInterface { + const res = instance!.appContext.config.vapor + if (__DEV__ && !res) { + warn( + `Vapor component found in vdom tree but vapor-in-vdom interop was not installed. ` + + `Make sure to install it:\n` + + `\`\`\`\nimport { vaporInteropPlugin } from 'vue'\n` + + `app.use(vaporInteropPlugin)\n` + + `\`\`\``, + ) + } + return res! +} diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index a2f66f7b8e..c34eb05a05 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -41,14 +41,14 @@ export function makeRender( let app: App function render( - props: RawProps = {}, + props: RawProps | undefined = undefined, container: string | ParentNode = host, ) { create(props) return mount(container) } - function create(props: RawProps = {}) { + function create(props: RawProps | undefined = undefined) { app?.unmount() app = createVaporApp(component, props) return res() diff --git a/packages/runtime-vapor/__tests__/apiWatch.spec.ts b/packages/runtime-vapor/__tests__/apiWatch.spec.ts index 01b83de80a..068791b8ad 100644 --- a/packages/runtime-vapor/__tests__/apiWatch.spec.ts +++ b/packages/runtime-vapor/__tests__/apiWatch.spec.ts @@ -297,7 +297,8 @@ describe('apiWatch', () => { } define(Comp).render() // should not record watcher in detached scope - expect(instance!.scope.effects.length).toBe(0) + // the 1 is the props validation effect + expect(instance!.scope.effects.length).toBe(1) }) test('watchEffect should keep running if created in a detached scope', async () => { diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index c9f3a5ffd6..3a945bc096 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -274,7 +274,8 @@ describe('component', () => { }).render() const i = instance as VaporComponentInstance - expect(i.scope.effects.length).toBe(2) + // watchEffect + renderEffect + props validation effect + expect(i.scope.effects.length).toBe(3) expect(host.innerHTML).toBe('
0
') app.unmount() diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 58341312fc..06f169c3e1 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -33,6 +33,7 @@ import { remove, } from './block' import { + type ShallowRef, markRaw, pauseTracking, proxyRefs, @@ -180,6 +181,10 @@ export function createComponent( simpleSetCurrentInstance(instance) pauseTracking() + if (__DEV__) { + setupPropsValidation(instance) + } + const setupFn = isFunction(component) ? component : component.setup const setupResult = setupFn ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [ @@ -295,6 +300,7 @@ export class VaporComponentInstance implements GenericComponentInstance { props: Record attrs: Record propsDefaults: Record | null + rawPropsRef?: ShallowRef // to hold vnode props in vdom interop mode slots: StaticSlots @@ -414,8 +420,6 @@ export class VaporComponentInstance implements GenericComponentInstance { : EMPTY_OBJ if (__DEV__) { - // validate props - if (rawProps) setupPropsValidation(this) // cache normalized options for dev only emit check this.propsOptions = normalizePropsOptions(comp) this.emitsOptions = normalizeEmitsOptions(comp) @@ -518,6 +522,7 @@ export function unmountComponent( // and also remove it from the parent's children list. remove(instance.block, parentNode) const parentInstance = instance.parent + instance.parent = null if (isVaporComponent(parentInstance)) { if (parentsWithUnmountedChildren) { // for optimize children removal @@ -525,7 +530,6 @@ export function unmountComponent( } else { removeItem(parentInstance.children, instance) } - instance.parent = null } } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index b228217efa..5e96f98621 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -4,7 +4,13 @@ export { defineVaporComponent } from './apiDefineComponent' // compiler-use only export { insert, prepend, remove } from './block' -export { createComponent, createComponentWithFallback } from './component' +export { + createComponent, + createComponentWithFallback, + mountComponent, + unmountComponent, + type VaporComponentInstance, +} from './component' export { renderEffect } from './renderEffect' export { createSlot } from './componentSlots' export { template, children, next } from './dom/template' @@ -38,3 +44,4 @@ export { applySelectModel, applyDynamicModel, } from './directives/vModel' +export { vaporInteropPlugin } from './vdomInterop' diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts new file mode 100644 index 0000000000..af85cfa061 --- /dev/null +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -0,0 +1,59 @@ +import { + type GenericComponentInstance, + type Plugin, + type VNode, + type VaporInVDOMInterface, + currentInstance, + shallowRef, + simpleSetCurrentInstance, +} from '@vue/runtime-dom' +import { + type VaporComponentInstance, + createComponent, + mountComponent, + unmountComponent, +} from './component' +import { insert } from './block' + +const vaporInVDOMInterface: VaporInVDOMInterface = { + mount( + vnode: VNode, + container: ParentNode, + anchor: Node, + parentComponent: GenericComponentInstance | null, + ) { + const selfAnchor = (vnode.anchor = document.createComment('vapor')) + container.insertBefore(selfAnchor, anchor) + const prev = currentInstance + simpleSetCurrentInstance(parentComponent) + const propsRef = shallowRef(vnode.props) + // @ts-expect-error + const instance = (vnode.component = createComponent(vnode.type, { + $: [() => propsRef.value], + })) + instance.rawPropsRef = propsRef + mountComponent(instance, container, selfAnchor) + simpleSetCurrentInstance(prev) + return instance + }, + + update(n1: VNode, n2: VNode) { + n2.component = n1.component + ;(n2.component as any as VaporComponentInstance).rawPropsRef!.value = + n2.props + }, + + unmount(vnode: VNode, doRemove?: boolean) { + const container = doRemove ? vnode.anchor!.parentNode : undefined + unmountComponent(vnode.component as any, container) + }, + + move(vnode: VNode, container: ParentNode, anchor: Node) { + insert(vnode.component as any, container, anchor) + insert(vnode.anchor as any, container, anchor) + }, +} + +export const vaporInteropPlugin: Plugin = app => { + app.config.vapor = vaporInVDOMInterface +} diff --git a/playground/src/main.ts b/playground/src/main.ts index 9d682d9ffb..39c0d6fbe3 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1 +1,2 @@ -import './_entry' +// import './_entry' +import './interop'