]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): vapor slots in vdom
authorEvan You <evan@vuejs.org>
Fri, 7 Feb 2025 07:47:06 +0000 (15:47 +0800)
committerEvan You <evan@vuejs.org>
Fri, 7 Feb 2025 13:32:22 +0000 (21:32 +0800)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/hmr.ts
packages/runtime-vapor/src/renderEffect.ts
packages/runtime-vapor/src/vdomInterop.ts

index a76758d4841e94c6a48aaef64f8e4f27a01fd2bd..30042368aff7eaf93d2233c3ce835d84e17d791b 100644 (file)
@@ -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: (
index 005e5dd3eeb4079de778764f3b07f150bee122a7..f6ff8803c8742a536ce7ab1b7f501456e32d3611 100644 (file)
@@ -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)
index a043ab8d5140cadb92e512b4dded0b3c076c48c1..152c5a4b81c13735825feb6e6d182ad08e2b0136 100644 (file)
@@ -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 ` +
index dee3010262dd885684b7b3aea4b8cebf237c5d8d..c7150e38e808c8cbf4ee1df47d070984d2bfe8bb 100644 (file)
@@ -505,7 +505,7 @@ export { type VaporInteropInterface } from './apiCreateApp'
 /**
  * @internal
  */
-export { type RendererInternals } from './renderer'
+export { type RendererInternals, MoveType } from './renderer'
 /**
  * @internal
  */
index d711218886ab9a0206470266daf74bfdc6716a25..fcbfdd0426cd28edbde6fa3b1db823a058248e55 100644 (file)
@@ -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)
       }
index 2d476bd4af0e2c2b187b6410cf8dfc725e966845..4b31151da228a0aa7e1f81e1f3e738f7aed434fa 100644 (file)
@@ -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<any>
+  }
+  /**
+   * @internal Vapor slot Block
+   */
+  vb?: any
 }
 
 // Since v-if and v-for are the two possible ways node structure can dynamically
index b33e33a029864d4441dfb46deb314ccb89567e62..0b8bdee0284404a02499fbe97fdc622b4071eff1 100644 (file)
@@ -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 {
index b67b6e98e80c99eb5f825a155c94cb3a10548948..9f6c2ba5a0dc56010f259dde510c4a23b7fa4470 100644 (file)
@@ -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,
index eb2d937735f629185c88e22a75f176975f513fb0..741f385861db5efe5eb40f569f8f37596c541971 100644 (file)
@@ -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,
index 0e4556fe1e0b11dad9c823ccfddcaac111975a44..a9fa9b33562dd9e6c9fad6348951905b772fef15 100644 (file)
@@ -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
index 76fecff67d9d8d76087acaaf9aea151578e2856c..68cb00956f1e2b27df64f2718a65ab0e035ca5cc 100644 (file)
@@ -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<Record<string, any>>
+> = {
+  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<any> = {
+  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<Slots>,
@@ -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 => {