Skip to content

Commit ff5a06c

Browse files
authored
feat(runtime-vapor): add support for v-once (#13459)
1 parent 1e1e13a commit ff5a06c

File tree

8 files changed

+156
-8
lines changed

8 files changed

+156
-8
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ describe('api: createDynamicComponent', () => {
6363
expect(html()).toBe('<baz></baz><!--dynamic-component-->')
6464
})
6565

66+
test('with v-once', async () => {
67+
const val = shallowRef<any>(A)
68+
69+
const { html } = define({
70+
setup() {
71+
return createDynamicComponent(() => val.value, null, null, true, true)
72+
},
73+
}).render()
74+
75+
expect(html()).toBe('AAA<!--dynamic-component-->')
76+
77+
val.value = B
78+
await nextTick()
79+
expect(html()).toBe('AAA<!--dynamic-component-->') // still AAA
80+
})
81+
82+
test('fallback with v-once', async () => {
83+
const val = shallowRef<any>('button')
84+
const id = ref(0)
85+
const { html } = define({
86+
setup() {
87+
return createDynamicComponent(
88+
() => val.value,
89+
{ id: () => id.value },
90+
null,
91+
true,
92+
true,
93+
)
94+
},
95+
}).render()
96+
97+
expect(html()).toBe('<button id="0"></button><!--dynamic-component-->')
98+
99+
id.value++
100+
await nextTick()
101+
expect(html()).toBe('<button id="0"></button><!--dynamic-component-->')
102+
})
103+
66104
test('render fallback with insertionState', async () => {
67105
const { html, mount } = define({
68106
setup() {

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import {
88
onUpdated,
99
provide,
1010
ref,
11+
useAttrs,
1112
watch,
1213
watchEffect,
1314
} from '@vue/runtime-dom'
1415
import {
1516
createComponent,
1617
createIf,
1718
createTextNode,
19+
defineVaporComponent,
1820
renderEffect,
1921
setInsertionState,
2022
template,
@@ -315,6 +317,66 @@ describe('component', () => {
315317
expect(getEffectsCount(i.scope)).toBe(0)
316318
})
317319

320+
it('work with v-once + props', () => {
321+
const Child = defineVaporComponent({
322+
props: {
323+
count: Number,
324+
},
325+
setup(props) {
326+
const n0 = template(' ')() as any
327+
renderEffect(() => setText(n0, props.count))
328+
return n0
329+
},
330+
})
331+
332+
const count = ref(0)
333+
const { html } = define({
334+
setup() {
335+
return createComponent(
336+
Child,
337+
{ count: () => count.value },
338+
null,
339+
true,
340+
true, // v-once
341+
)
342+
},
343+
}).render()
344+
345+
expect(html()).toBe('0')
346+
347+
count.value++
348+
expect(html()).toBe('0')
349+
})
350+
351+
it('work with v-once + attrs', () => {
352+
const Child = defineVaporComponent({
353+
setup() {
354+
const attrs = useAttrs()
355+
const n0 = template(' ')() as any
356+
renderEffect(() => setText(n0, attrs.count as string))
357+
return n0
358+
},
359+
})
360+
361+
const count = ref(0)
362+
const { html } = define({
363+
setup() {
364+
return createComponent(
365+
Child,
366+
{ count: () => count.value },
367+
null,
368+
true,
369+
true, // v-once
370+
)
371+
},
372+
}).render()
373+
374+
expect(html()).toBe('0')
375+
376+
count.value++
377+
expect(html()).toBe('0')
378+
})
379+
318380
test('should mount component only with template in production mode', () => {
319381
__DEV__ = false
320382
const { component: Child } = define({

packages/runtime-vapor/src/apiCreateApp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
4141
app._props as RawProps,
4242
null,
4343
false,
44+
false,
4445
app._context,
4546
)
4647
mountComponent(instance, container)
@@ -61,6 +62,7 @@ const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
6162
app._props as RawProps,
6263
null,
6364
false,
65+
false,
6466
app._context,
6567
)
6668
mountComponent(instance, container)

packages/runtime-vapor/src/apiCreateDynamicComponent.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function createDynamicComponent(
1818
rawProps?: RawProps | null,
1919
rawSlots?: RawSlots | null,
2020
isSingleRoot?: boolean,
21+
once?: boolean,
2122
): VaporFragment {
2223
const _insertionParent = insertionParent
2324
const _insertionAnchor = insertionAnchor
@@ -29,7 +30,7 @@ export function createDynamicComponent(
2930
? new DynamicFragment('dynamic-component')
3031
: new DynamicFragment()
3132

32-
renderEffect(() => {
33+
const renderFn = () => {
3334
const value = getter()
3435
const appContext =
3536
(currentInstance && currentInstance.appContext) || emptyContext
@@ -40,11 +41,15 @@ export function createDynamicComponent(
4041
rawProps,
4142
rawSlots,
4243
isSingleRoot,
44+
once,
4345
appContext,
4446
),
4547
value,
4648
)
47-
})
49+
}
50+
51+
if (once) renderFn()
52+
else renderEffect(renderFn)
4853

4954
if (!isHydrating) {
5055
if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)

packages/runtime-vapor/src/apiDefineAsyncComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ function createInnerComp(
185185
rawProps,
186186
rawSlots,
187187
isSingleRoot,
188+
undefined,
188189
appContext,
189190
)
190191

packages/runtime-vapor/src/component.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
getPropsProxyHandlers,
5252
hasFallthroughAttrs,
5353
normalizePropsOptions,
54+
resolveDynamicProps,
5455
setupPropsValidation,
5556
} from './componentProps'
5657
import { type RenderEffect, renderEffect } from './renderEffect'
@@ -163,6 +164,7 @@ export function createComponent(
163164
rawProps?: LooseRawProps | null,
164165
rawSlots?: LooseRawSlots | null,
165166
isSingleRoot?: boolean,
167+
once?: boolean,
166168
appContext: GenericAppContext = (currentInstance &&
167169
currentInstance.appContext) ||
168170
emptyContext,
@@ -246,6 +248,7 @@ export function createComponent(
246248
rawProps as RawProps,
247249
rawSlots as RawSlots,
248250
appContext,
251+
once,
249252
)
250253

251254
// HMR
@@ -521,6 +524,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
521524
rawProps?: RawProps | null,
522525
rawSlots?: RawSlots | null,
523526
appContext?: GenericAppContext,
527+
once?: boolean,
524528
) {
525529
this.vapor = true
526530
this.uid = nextUid()
@@ -561,7 +565,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
561565
this.rawProps = rawProps || EMPTY_OBJ
562566
this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
563567
if (rawProps || comp.props) {
564-
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
568+
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp, once)
565569
this.attrs = new Proxy(this, attrsHandlers)
566570
this.props = comp.props
567571
? new Proxy(this, propsHandlers!)
@@ -606,10 +610,18 @@ export function createComponentWithFallback(
606610
rawProps?: LooseRawProps | null,
607611
rawSlots?: LooseRawSlots | null,
608612
isSingleRoot?: boolean,
613+
once?: boolean,
609614
appContext?: GenericAppContext,
610615
): HTMLElement | VaporComponentInstance {
611616
if (!isString(comp)) {
612-
return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext)
617+
return createComponent(
618+
comp,
619+
rawProps,
620+
rawSlots,
621+
isSingleRoot,
622+
once,
623+
appContext,
624+
)
613625
}
614626

615627
const _insertionParent = insertionParent
@@ -628,6 +640,13 @@ export function createComponentWithFallback(
628640
// mark single root
629641
;(el as any).$root = isSingleRoot
630642

643+
if (rawProps) {
644+
const setFn = () =>
645+
setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
646+
if (once) setFn()
647+
else renderEffect(setFn)
648+
}
649+
631650
if (rawSlots) {
632651
let nextNode: Node | null = null
633652
if (isHydrating) {

packages/runtime-vapor/src/componentProps.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { ReactiveFlags } from '@vue/reactivity'
2424
import { normalizeEmitsOptions } from './componentEmits'
2525
import { renderEffect } from './renderEffect'
26+
import { pauseTracking, resetTracking } from '@vue/reactivity'
2627
import type { interopKey } from './vdomInterop'
2728

2829
export type RawProps = Record<string, () => unknown> & {
@@ -43,6 +44,7 @@ export function resolveSource(
4344

4445
export function getPropsProxyHandlers(
4546
comp: VaporComponent,
47+
once?: boolean,
4648
): [
4749
ProxyHandler<VaporComponentInstance> | null,
4850
ProxyHandler<VaporComponentInstance>,
@@ -111,17 +113,26 @@ export function getPropsProxyHandlers(
111113
)
112114
}
113115

116+
const getPropValue = once
117+
? (...args: Parameters<typeof getProp>) => {
118+
pauseTracking()
119+
const value = getProp(...args)
120+
resetTracking()
121+
return value
122+
}
123+
: getProp
124+
114125
const propsHandlers = propsOptions
115126
? ({
116-
get: (target, key) => getProp(target, key),
127+
get: (target, key) => getPropValue(target, key),
117128
has: (_, key) => isProp(key),
118129
ownKeys: () => Object.keys(propsOptions),
119130
getOwnPropertyDescriptor(target, key) {
120131
if (isProp(key)) {
121132
return {
122133
configurable: true,
123134
enumerable: true,
124-
get: () => getProp(target, key),
135+
get: () => getPropValue(target, key),
125136
}
126137
}
127138
},
@@ -149,16 +160,25 @@ export function getPropsProxyHandlers(
149160
}
150161
}
151162

163+
const getAttrValue = once
164+
? (...args: Parameters<typeof getAttr>) => {
165+
pauseTracking()
166+
const value = getAttr(...args)
167+
resetTracking()
168+
return value
169+
}
170+
: getAttr
171+
152172
const attrsHandlers = {
153-
get: (target, key: string) => getAttr(target.rawProps, key),
173+
get: (target, key: string) => getAttrValue(target.rawProps, key),
154174
has: (target, key: string) => hasAttr(target.rawProps, key),
155175
ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
156176
getOwnPropertyDescriptor(target, key: string) {
157177
if (hasAttr(target.rawProps, key)) {
158178
return {
159179
configurable: true,
160180
enumerable: true,
161-
get: () => getAttr(target.rawProps, key),
181+
get: () => getAttrValue(target.rawProps, key),
162182
}
163183
}
164184
},

packages/runtime-vapor/src/vdomInterop.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ const vaporInteropImpl: Omit<
124124
_: slotsRef, // pass the slots ref
125125
} as any as RawSlots,
126126
undefined,
127+
undefined,
127128
(parentComponent ? parentComponent.appContext : vnode.appContext) as any,
128129
))
129130
instance.rawPropsRef = propsRef

0 commit comments

Comments
 (0)