Skip to content

Commit be2b79d

Browse files
authored
fix(runtime-vapor): implement v-once caching for props and attrs (#14207)
1 parent 55d7507 commit be2b79d

File tree

3 files changed

+107
-19
lines changed

3 files changed

+107
-19
lines changed

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,83 @@ describe('component', () => {
377377
expect(html()).toBe('0')
378378
})
379379

380+
it('v-once props should be frozen and not update when parent changes', async () => {
381+
const localCount = ref(0)
382+
const Child = defineVaporComponent({
383+
props: {
384+
count: Number,
385+
},
386+
setup(props) {
387+
const n0 = template('<div></div>')() as any
388+
renderEffect(() =>
389+
setElementText(n0, `${localCount.value} - ${props.count}`),
390+
)
391+
return n0
392+
},
393+
})
394+
395+
const parentCount = ref(0)
396+
const { html } = define({
397+
setup() {
398+
return createComponent(
399+
Child,
400+
{ count: () => parentCount.value },
401+
null,
402+
true,
403+
true, // v-once
404+
)
405+
},
406+
}).render()
407+
408+
expect(html()).toBe('<div>0 - 0</div>')
409+
410+
parentCount.value++
411+
await nextTick()
412+
expect(html()).toBe('<div>0 - 0</div>')
413+
414+
localCount.value++
415+
await nextTick()
416+
expect(html()).toBe('<div>1 - 0</div>')
417+
})
418+
419+
it('v-once attrs should be frozen and not update when parent changes', async () => {
420+
const localCount = ref(0)
421+
const Child = defineVaporComponent({
422+
inheritAttrs: false,
423+
setup() {
424+
const attrs = useAttrs()
425+
const n0 = template('<div></div>')() as any
426+
renderEffect(() =>
427+
setElementText(n0, `${localCount.value} - ${attrs.count}`),
428+
)
429+
return n0
430+
},
431+
})
432+
433+
const parentCount = ref(0)
434+
const { html } = define({
435+
setup() {
436+
return createComponent(
437+
Child,
438+
{ count: () => parentCount.value },
439+
null,
440+
true,
441+
true, // v-once
442+
)
443+
},
444+
}).render()
445+
446+
expect(html()).toBe('<div>0 - 0</div>')
447+
448+
parentCount.value++
449+
await nextTick()
450+
expect(html()).toBe('<div>0 - 0</div>')
451+
452+
localCount.value++
453+
await nextTick()
454+
expect(html()).toBe('<div>1 - 0</div>')
455+
})
456+
380457
test('should mount component only with template in production mode', () => {
381458
__DEV__ = false
382459
const { component: Child } = define({

packages/runtime-vapor/src/component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,10 @@ export class VaporComponentInstance<
600600
// for keep-alive
601601
shapeFlag?: number
602602

603+
// for v-once: caches props/attrs values to ensure they remain frozen
604+
// even when the component re-renders due to local state changes
605+
oncePropsCache?: Record<string | symbol, any>
606+
603607
// lifecycle hooks
604608
isMounted: boolean
605609
isUnmounted: boolean

packages/runtime-vapor/src/componentProps.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,26 +113,37 @@ export function getPropsProxyHandlers(
113113
)
114114
}
115115

116-
const getPropValue = once
117-
? (...args: Parameters<typeof getProp>) => {
116+
const withOnceCache = <
117+
T extends (instance: VaporComponentInstance, key: string | symbol) => any,
118+
>(
119+
getter: T,
120+
): T => {
121+
return ((instance: VaporComponentInstance, key: string | symbol) => {
122+
const cache = instance.oncePropsCache || (instance.oncePropsCache = {})
123+
if (!(key in cache)) {
118124
pauseTracking()
119-
const value = getProp(...args)
120-
resetTracking()
121-
return value
125+
try {
126+
cache[key] = getter(instance, key)
127+
} finally {
128+
resetTracking()
129+
}
122130
}
123-
: getProp
131+
return cache[key]
132+
}) as T
133+
}
124134

135+
const getOnceProp = withOnceCache(getProp)
125136
const propsHandlers = propsOptions
126137
? ({
127-
get: (target, key) => getPropValue(target, key),
138+
get: (target, key) => (once ? getOnceProp : getProp)(target, key),
128139
has: (_, key) => isProp(key),
129140
ownKeys: () => Object.keys(propsOptions),
130141
getOwnPropertyDescriptor(target, key) {
131142
if (isProp(key)) {
132143
return {
133144
configurable: true,
134145
enumerable: true,
135-
get: () => getPropValue(target, key),
146+
get: () => (once ? getOnceProp : getProp)(target, key),
136147
}
137148
}
138149
},
@@ -160,25 +171,21 @@ export function getPropsProxyHandlers(
160171
}
161172
}
162173

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-
174+
const getOnceAttr = withOnceCache((instance, key) =>
175+
getAttr(instance.rawProps, key as string),
176+
)
172177
const attrsHandlers = {
173-
get: (target, key: string) => getAttrValue(target.rawProps, key),
178+
get: (target, key: string) =>
179+
once ? getOnceAttr(target, key) : getAttr(target.rawProps, key),
174180
has: (target, key: string) => hasAttr(target.rawProps, key),
175181
ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
176182
getOwnPropertyDescriptor(target, key: string) {
177183
if (hasAttr(target.rawProps, key)) {
178184
return {
179185
configurable: true,
180186
enumerable: true,
181-
get: () => getAttrValue(target.rawProps, key),
187+
get: () =>
188+
once ? getOnceAttr(target, key) : getAttr(target.rawProps, key),
182189
}
183190
}
184191
},

0 commit comments

Comments
 (0)