Skip to content

Commit 498ce69

Browse files
authored
feat(vapor): implement v-once support for slot outlets (#14141)
1 parent 1d72706 commit 498ce69

File tree

8 files changed

+75
-3
lines changed

8 files changed

+75
-3
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ export function render(_ctx) {
6060
}"
6161
`;
6262

63+
exports[`compiler: v-once > on slot outlet 1`] = `
64+
"import { setInsertionState as _setInsertionState, createSlot as _createSlot, template as _template } from 'vue';
65+
const t0 = _template("<div></div>", true)
66+
67+
export function render(_ctx) {
68+
const n1 = t0()
69+
_setInsertionState(n1, null, true)
70+
const n0 = _createSlot("default", null, null, null, true)
71+
return n1
72+
}"
73+
`;
74+
6375
exports[`compiler: v-once > with v-for 1`] = `
6476
"import { createFor as _createFor, template as _template } from 'vue';
6577
const t0 = _template("<div></div>")

packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,13 @@ describe('compiler: v-once', () => {
135135
})
136136
})
137137

138-
test.todo('on slot outlet')
138+
test('on slot outlet', () => {
139+
const { ir, code } = compileWithOnce(`<div><slot v-once /></div>`)
140+
expect(code).toMatchSnapshot()
141+
142+
expect(ir.block.effect).lengthOf(0)
143+
expect(ir.block.operation).lengthOf(0)
144+
})
139145

140146
test('inside v-once', () => {
141147
const { ir, code } = compileWithOnce(`<div v-once><div v-once/></div>`)

packages/compiler-vapor/src/generators/slotOutlet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function genSlotOutlet(
1010
context: CodegenContext,
1111
): CodeFragment[] {
1212
const { helper } = context
13-
const { id, name, fallback, noSlotted } = oper
13+
const { id, name, fallback, noSlotted, once } = oper
1414
const [frag, push] = buildCodeFragment()
1515

1616
const nameExpr = name.isStatic
@@ -31,6 +31,7 @@ export function genSlotOutlet(
3131
genRawProps(oper.props, context) || 'null',
3232
fallbackArg,
3333
noSlotted && 'true', // noSlotted
34+
once && 'true', // v-once
3435
),
3536
)
3637

packages/compiler-vapor/src/ir/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
221221
props: IRProps[]
222222
fallback?: BlockIRNode
223223
noSlotted?: boolean
224+
once?: boolean
224225
parent?: number
225226
anchor?: number
226227
append?: boolean

packages/compiler-vapor/src/transforms/transformSlotOutlet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
107107
props: irProps,
108108
fallback,
109109
noSlotted: !!(context.options.scopeId && !context.options.slotted),
110+
once: context.inVOnce,
110111
}
111112
}
112113
}

packages/runtime-vapor/__tests__/componentSlots.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
renderEffect,
1515
setInsertionState,
1616
template,
17+
txt,
1718
vaporInteropPlugin,
1819
withVaporCtx,
1920
} from '../src'
@@ -774,6 +775,42 @@ describe('component: slots', () => {
774775
await nextTick()
775776
expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
776777
})
778+
779+
test('work with v-once', async () => {
780+
const Child = defineVaporComponent({
781+
setup() {
782+
return createSlot(
783+
'default',
784+
null,
785+
undefined,
786+
undefined,
787+
true /* once */,
788+
)
789+
},
790+
})
791+
792+
const count = ref(0)
793+
794+
const { html } = define({
795+
setup() {
796+
return createComponent(Child, null, {
797+
default: withVaporCtx(() => {
798+
const n3 = template('<div> </div>')() as any
799+
const x3 = txt(n3) as any
800+
renderEffect(() => setText(x3, toDisplayString(count.value)))
801+
return n3
802+
}),
803+
})
804+
},
805+
}).render()
806+
807+
expect(html()).toBe('<div>0</div><!--slot-->')
808+
809+
// expect no changes due to v-once
810+
count.value++
811+
await nextTick()
812+
expect(html()).toBe('<div>0</div><!--slot-->')
813+
})
777814
})
778815

779816
describe('forwarded slot', () => {

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ import { DynamicFragment, type VaporFragment } from './fragment'
2525
import { createElement } from './dom/node'
2626
import { setDynamicProps } from './dom/prop'
2727

28+
/**
29+
* Flag to indicate if we are executing a once slot.
30+
* When true, renderEffect should skip creating reactive effect.
31+
*/
32+
export let inOnceSlot = false
33+
2834
/**
2935
* Current slot scopeIds for vdom interop
3036
*/
@@ -163,6 +169,7 @@ export function createSlot(
163169
rawProps?: LooseRawProps | null,
164170
fallback?: VaporSlot,
165171
noSlotted?: boolean,
172+
once?: boolean,
166173
): Block {
167174
const _insertionParent = insertionParent
168175
const _insertionAnchor = insertionAnchor
@@ -236,9 +243,12 @@ export function createSlot(
236243
const prevSlotScopeIds = setCurrentSlotScopeIds(
237244
slotScopeIds.length > 0 ? slotScopeIds : null,
238245
)
246+
const prev = inOnceSlot
239247
try {
248+
if (once) inOnceSlot = true
240249
return slot(slotProps)
241250
} finally {
251+
inOnceSlot = prev
242252
setCurrentSlotScopeIds(prevSlotScopeIds)
243253
}
244254
}),
@@ -249,7 +259,7 @@ export function createSlot(
249259
}
250260

251261
// dynamic slot name or has dynamicSlots
252-
if (isDynamicName || rawSlots.$) {
262+
if (!once && (isDynamicName || rawSlots.$)) {
253263
renderEffect(renderSlot)
254264
} else {
255265
renderSlot()

packages/runtime-vapor/src/renderEffect.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
warn,
1010
} from '@vue/runtime-dom'
1111
import { type VaporComponentInstance, isVaporComponent } from './component'
12+
import { inOnceSlot } from './componentSlots'
1213
import { invokeArrayFns } from '@vue/shared'
1314

1415
export class RenderEffect extends ReactiveEffect {
@@ -88,6 +89,9 @@ export class RenderEffect extends ReactiveEffect {
8889
}
8990

9091
export function renderEffect(fn: () => void, noLifecycle = false): void {
92+
// in once slot, just run the function directly
93+
if (inOnceSlot) return fn()
94+
9195
const effect = new RenderEffect(fn)
9296
if (noLifecycle) {
9397
effect.fn = fn

0 commit comments

Comments
 (0)