diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
index 7f76c3cd7c1..eb0abd04676 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
+++ b/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("
", 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`] = `
"import { createFor as _createFor, template as _template } from 'vue';
const t0 = _template("")
diff --git a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
index 97f6ee62a11..4a42901a1c2 100644
--- a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
+++ b/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(`
`)
+ expect(code).toMatchSnapshot()
+
+ expect(ir.block.effect).lengthOf(0)
+ expect(ir.block.operation).lengthOf(0)
+ })
test('inside v-once', () => {
const { ir, code } = compileWithOnce(``)
diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts
index afacb644888..68428b2331b 100644
--- a/packages/compiler-vapor/src/generators/slotOutlet.ts
+++ b/packages/compiler-vapor/src/generators/slotOutlet.ts
@@ -10,7 +10,7 @@ export function genSlotOutlet(
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
- const { id, name, fallback, noSlotted } = oper
+ const { id, name, fallback, noSlotted, once } = oper
const [frag, push] = buildCodeFragment()
const nameExpr = name.isStatic
@@ -31,6 +31,7 @@ export function genSlotOutlet(
genRawProps(oper.props, context) || 'null',
fallbackArg,
noSlotted && 'true', // noSlotted
+ once && 'true', // v-once
),
)
diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts
index 13fd5e1e696..115310bbda3 100644
--- a/packages/compiler-vapor/src/ir/index.ts
+++ b/packages/compiler-vapor/src/ir/index.ts
@@ -221,6 +221,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
props: IRProps[]
fallback?: BlockIRNode
noSlotted?: boolean
+ once?: boolean
parent?: number
anchor?: number
append?: boolean
diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts
index 88edc0510e3..72dc7875da8 100644
--- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts
+++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts
@@ -107,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
props: irProps,
fallback,
noSlotted: !!(context.options.scopeId && !context.options.slotted),
+ once: context.inVOnce,
}
}
}
diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
index 69756b02339..74b6a9b3b9a 100644
--- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
@@ -14,6 +14,7 @@ import {
renderEffect,
setInsertionState,
template,
+ txt,
vaporInteropPlugin,
withVaporCtx,
} from '../src'
@@ -774,6 +775,42 @@ describe('component: slots', () => {
await nextTick()
expect(html()).toBe('2')
})
+
+ 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('
')() as any
+ const x3 = txt(n3) as any
+ renderEffect(() => setText(x3, toDisplayString(count.value)))
+ return n3
+ }),
+ })
+ },
+ }).render()
+
+ expect(html()).toBe('0
')
+
+ // expect no changes due to v-once
+ count.value++
+ await nextTick()
+ expect(html()).toBe('0
')
+ })
})
describe('forwarded slot', () => {
diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts
index 2f83b9f8647..b450e077a14 100644
--- a/packages/runtime-vapor/src/componentSlots.ts
+++ b/packages/runtime-vapor/src/componentSlots.ts
@@ -25,6 +25,12 @@ import { DynamicFragment, type VaporFragment } from './fragment'
import { createElement } from './dom/node'
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
*/
@@ -163,6 +169,7 @@ export function createSlot(
rawProps?: LooseRawProps | null,
fallback?: VaporSlot,
noSlotted?: boolean,
+ once?: boolean,
): Block {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
@@ -236,9 +243,12 @@ export function createSlot(
const prevSlotScopeIds = setCurrentSlotScopeIds(
slotScopeIds.length > 0 ? slotScopeIds : null,
)
+ const prev = inOnceSlot
try {
+ if (once) inOnceSlot = true
return slot(slotProps)
} finally {
+ inOnceSlot = prev
setCurrentSlotScopeIds(prevSlotScopeIds)
}
}),
@@ -249,7 +259,7 @@ export function createSlot(
}
// dynamic slot name or has dynamicSlots
- if (isDynamicName || rawSlots.$) {
+ if (!once && (isDynamicName || rawSlots.$)) {
renderEffect(renderSlot)
} else {
renderSlot()
diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts
index e36ac4ba458..5fc2d541424 100644
--- a/packages/runtime-vapor/src/renderEffect.ts
+++ b/packages/runtime-vapor/src/renderEffect.ts
@@ -9,6 +9,7 @@ import {
warn,
} from '@vue/runtime-dom'
import { type VaporComponentInstance, isVaporComponent } from './component'
+import { inOnceSlot } from './componentSlots'
import { invokeArrayFns } from '@vue/shared'
export class RenderEffect extends ReactiveEffect {
@@ -88,6 +89,9 @@ export class RenderEffect extends ReactiveEffect {
}
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)
if (noLifecycle) {
effect.fn = fn