Przeglądaj źródła

feat(vapor): implement `v-once` support for slot outlets (#14141)

edison 5 miesięcy temu
rodzic
commit
498ce69a58

+ 12 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap

@@ -60,6 +60,18 @@ export function render(_ctx) {
 }"
 }"
 `;
 `;
 
 
+exports[`compiler: v-once > on slot outlet 1`] = `
+"import { setInsertionState as _setInsertionState, createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  _setInsertionState(n1, null, true)
+  const n0 = _createSlot("default", null, null, null, true)
+  return n1
+}"
+`;
+
 exports[`compiler: v-once > with v-for 1`] = `
 exports[`compiler: v-once > with v-for 1`] = `
 "import { createFor as _createFor, template as _template } from 'vue';
 "import { createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div></div>")
 const t0 = _template("<div></div>")

+ 7 - 1
packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts

@@ -135,7 +135,13 @@ describe('compiler: v-once', () => {
     })
     })
   })
   })
 
 
-  test.todo('on slot outlet')
+  test('on slot outlet', () => {
+    const { ir, code } = compileWithOnce(`<div><slot v-once /></div>`)
+    expect(code).toMatchSnapshot()
+
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).lengthOf(0)
+  })
 
 
   test('inside v-once', () => {
   test('inside v-once', () => {
     const { ir, code } = compileWithOnce(`<div v-once><div v-once/></div>`)
     const { ir, code } = compileWithOnce(`<div v-once><div v-once/></div>`)

+ 2 - 1
packages/compiler-vapor/src/generators/slotOutlet.ts

@@ -10,7 +10,7 @@ export function genSlotOutlet(
   context: CodegenContext,
   context: CodegenContext,
 ): CodeFragment[] {
 ): CodeFragment[] {
   const { helper } = context
   const { helper } = context
-  const { id, name, fallback, noSlotted } = oper
+  const { id, name, fallback, noSlotted, once } = oper
   const [frag, push] = buildCodeFragment()
   const [frag, push] = buildCodeFragment()
 
 
   const nameExpr = name.isStatic
   const nameExpr = name.isStatic
@@ -31,6 +31,7 @@ export function genSlotOutlet(
       genRawProps(oper.props, context) || 'null',
       genRawProps(oper.props, context) || 'null',
       fallbackArg,
       fallbackArg,
       noSlotted && 'true', // noSlotted
       noSlotted && 'true', // noSlotted
+      once && 'true', // v-once
     ),
     ),
   )
   )
 
 

+ 1 - 0
packages/compiler-vapor/src/ir/index.ts

@@ -221,6 +221,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
   props: IRProps[]
   props: IRProps[]
   fallback?: BlockIRNode
   fallback?: BlockIRNode
   noSlotted?: boolean
   noSlotted?: boolean
+  once?: boolean
   parent?: number
   parent?: number
   anchor?: number
   anchor?: number
   append?: boolean
   append?: boolean

+ 1 - 0
packages/compiler-vapor/src/transforms/transformSlotOutlet.ts

@@ -107,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
       props: irProps,
       props: irProps,
       fallback,
       fallback,
       noSlotted: !!(context.options.scopeId && !context.options.slotted),
       noSlotted: !!(context.options.scopeId && !context.options.slotted),
+      once: context.inVOnce,
     }
     }
   }
   }
 }
 }

+ 37 - 0
packages/runtime-vapor/__tests__/componentSlots.spec.ts

@@ -14,6 +14,7 @@ import {
   renderEffect,
   renderEffect,
   setInsertionState,
   setInsertionState,
   template,
   template,
+  txt,
   vaporInteropPlugin,
   vaporInteropPlugin,
   withVaporCtx,
   withVaporCtx,
 } from '../src'
 } from '../src'
@@ -774,6 +775,42 @@ describe('component: slots', () => {
       await nextTick()
       await nextTick()
       expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
       expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
     })
     })
+
+    test('work with v-once', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot(
+            'default',
+            null,
+            undefined,
+            undefined,
+            true /* once */,
+          )
+        },
+      })
+
+      const count = ref(0)
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: withVaporCtx(() => {
+              const n3 = template('<div> </div>')() as any
+              const x3 = txt(n3) as any
+              renderEffect(() => setText(x3, toDisplayString(count.value)))
+              return n3
+            }),
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('<div>0</div><!--slot-->')
+
+      // expect no changes due to v-once
+      count.value++
+      await nextTick()
+      expect(html()).toBe('<div>0</div><!--slot-->')
+    })
   })
   })
 
 
   describe('forwarded slot', () => {
   describe('forwarded slot', () => {

+ 11 - 1
packages/runtime-vapor/src/componentSlots.ts

@@ -25,6 +25,12 @@ import { DynamicFragment, type VaporFragment } from './fragment'
 import { createElement } from './dom/node'
 import { createElement } from './dom/node'
 import { setDynamicProps } from './dom/prop'
 import { setDynamicProps } from './dom/prop'
 
 
+/**
+ * Flag to indicate if we are executing a once slot.
+ * When true, renderEffect should skip creating reactive effect.
+ */
+export let inOnceSlot = false
+
 /**
 /**
  * Current slot scopeIds for vdom interop
  * Current slot scopeIds for vdom interop
  */
  */
@@ -163,6 +169,7 @@ export function createSlot(
   rawProps?: LooseRawProps | null,
   rawProps?: LooseRawProps | null,
   fallback?: VaporSlot,
   fallback?: VaporSlot,
   noSlotted?: boolean,
   noSlotted?: boolean,
+  once?: boolean,
 ): Block {
 ): Block {
   const _insertionParent = insertionParent
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
   const _insertionAnchor = insertionAnchor
@@ -236,9 +243,12 @@ export function createSlot(
               const prevSlotScopeIds = setCurrentSlotScopeIds(
               const prevSlotScopeIds = setCurrentSlotScopeIds(
                 slotScopeIds.length > 0 ? slotScopeIds : null,
                 slotScopeIds.length > 0 ? slotScopeIds : null,
               )
               )
+              const prev = inOnceSlot
               try {
               try {
+                if (once) inOnceSlot = true
                 return slot(slotProps)
                 return slot(slotProps)
               } finally {
               } finally {
+                inOnceSlot = prev
                 setCurrentSlotScopeIds(prevSlotScopeIds)
                 setCurrentSlotScopeIds(prevSlotScopeIds)
               }
               }
             }),
             }),
@@ -249,7 +259,7 @@ export function createSlot(
     }
     }
 
 
     // dynamic slot name or has dynamicSlots
     // dynamic slot name or has dynamicSlots
-    if (isDynamicName || rawSlots.$) {
+    if (!once && (isDynamicName || rawSlots.$)) {
       renderEffect(renderSlot)
       renderEffect(renderSlot)
     } else {
     } else {
       renderSlot()
       renderSlot()

+ 4 - 0
packages/runtime-vapor/src/renderEffect.ts

@@ -9,6 +9,7 @@ import {
   warn,
   warn,
 } from '@vue/runtime-dom'
 } from '@vue/runtime-dom'
 import { type VaporComponentInstance, isVaporComponent } from './component'
 import { type VaporComponentInstance, isVaporComponent } from './component'
+import { inOnceSlot } from './componentSlots'
 import { invokeArrayFns } from '@vue/shared'
 import { invokeArrayFns } from '@vue/shared'
 
 
 export class RenderEffect extends ReactiveEffect {
 export class RenderEffect extends ReactiveEffect {
@@ -88,6 +89,9 @@ export class RenderEffect extends ReactiveEffect {
 }
 }
 
 
 export function renderEffect(fn: () => void, noLifecycle = false): void {
 export function renderEffect(fn: () => void, noLifecycle = false): void {
+  // in once slot, just run the function directly
+  if (inOnceSlot) return fn()
+
   const effect = new RenderEffect(fn)
   const effect = new RenderEffect(fn)
   if (noLifecycle) {
   if (noLifecycle) {
     effect.fn = fn
     effect.fn = fn