Răsfoiți Sursa

fix(runtime-vapor): implement v-once caching for props and attrs (#14207)

edison 4 luni în urmă
părinte
comite
be2b79dedd

+ 77 - 0
packages/runtime-vapor/__tests__/component.spec.ts

@@ -377,6 +377,83 @@ describe('component', () => {
     expect(html()).toBe('0')
   })
 
+  it('v-once props should be frozen and not update when parent changes', async () => {
+    const localCount = ref(0)
+    const Child = defineVaporComponent({
+      props: {
+        count: Number,
+      },
+      setup(props) {
+        const n0 = template('<div></div>')() as any
+        renderEffect(() =>
+          setElementText(n0, `${localCount.value} - ${props.count}`),
+        )
+        return n0
+      },
+    })
+
+    const parentCount = ref(0)
+    const { html } = define({
+      setup() {
+        return createComponent(
+          Child,
+          { count: () => parentCount.value },
+          null,
+          true,
+          true, // v-once
+        )
+      },
+    }).render()
+
+    expect(html()).toBe('<div>0 - 0</div>')
+
+    parentCount.value++
+    await nextTick()
+    expect(html()).toBe('<div>0 - 0</div>')
+
+    localCount.value++
+    await nextTick()
+    expect(html()).toBe('<div>1 - 0</div>')
+  })
+
+  it('v-once attrs should be frozen and not update when parent changes', async () => {
+    const localCount = ref(0)
+    const Child = defineVaporComponent({
+      inheritAttrs: false,
+      setup() {
+        const attrs = useAttrs()
+        const n0 = template('<div></div>')() as any
+        renderEffect(() =>
+          setElementText(n0, `${localCount.value} - ${attrs.count}`),
+        )
+        return n0
+      },
+    })
+
+    const parentCount = ref(0)
+    const { html } = define({
+      setup() {
+        return createComponent(
+          Child,
+          { count: () => parentCount.value },
+          null,
+          true,
+          true, // v-once
+        )
+      },
+    }).render()
+
+    expect(html()).toBe('<div>0 - 0</div>')
+
+    parentCount.value++
+    await nextTick()
+    expect(html()).toBe('<div>0 - 0</div>')
+
+    localCount.value++
+    await nextTick()
+    expect(html()).toBe('<div>1 - 0</div>')
+  })
+
   test('should mount component only with template in production mode', () => {
     __DEV__ = false
     const { component: Child } = define({

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

@@ -600,6 +600,10 @@ export class VaporComponentInstance<
   // for keep-alive
   shapeFlag?: number
 
+  // for v-once: caches props/attrs values to ensure they remain frozen
+  // even when the component re-renders due to local state changes
+  oncePropsCache?: Record<string | symbol, any>
+
   // lifecycle hooks
   isMounted: boolean
   isUnmounted: boolean

+ 26 - 19
packages/runtime-vapor/src/componentProps.ts

@@ -113,18 +113,29 @@ export function getPropsProxyHandlers(
     )
   }
 
-  const getPropValue = once
-    ? (...args: Parameters<typeof getProp>) => {
+  const withOnceCache = <
+    T extends (instance: VaporComponentInstance, key: string | symbol) => any,
+  >(
+    getter: T,
+  ): T => {
+    return ((instance: VaporComponentInstance, key: string | symbol) => {
+      const cache = instance.oncePropsCache || (instance.oncePropsCache = {})
+      if (!(key in cache)) {
         pauseTracking()
-        const value = getProp(...args)
-        resetTracking()
-        return value
+        try {
+          cache[key] = getter(instance, key)
+        } finally {
+          resetTracking()
+        }
       }
-    : getProp
+      return cache[key]
+    }) as T
+  }
 
+  const getOnceProp = withOnceCache(getProp)
   const propsHandlers = propsOptions
     ? ({
-        get: (target, key) => getPropValue(target, key),
+        get: (target, key) => (once ? getOnceProp : getProp)(target, key),
         has: (_, key) => isProp(key),
         ownKeys: () => Object.keys(propsOptions),
         getOwnPropertyDescriptor(target, key) {
@@ -132,7 +143,7 @@ export function getPropsProxyHandlers(
             return {
               configurable: true,
               enumerable: true,
-              get: () => getPropValue(target, key),
+              get: () => (once ? getOnceProp : getProp)(target, key),
             }
           }
         },
@@ -160,17 +171,12 @@ export function getPropsProxyHandlers(
     }
   }
 
-  const getAttrValue = once
-    ? (...args: Parameters<typeof getAttr>) => {
-        pauseTracking()
-        const value = getAttr(...args)
-        resetTracking()
-        return value
-      }
-    : getAttr
-
+  const getOnceAttr = withOnceCache((instance, key) =>
+    getAttr(instance.rawProps, key as string),
+  )
   const attrsHandlers = {
-    get: (target, key: string) => getAttrValue(target.rawProps, key),
+    get: (target, key: string) =>
+      once ? getOnceAttr(target, key) : getAttr(target.rawProps, key),
     has: (target, key: string) => hasAttr(target.rawProps, key),
     ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
     getOwnPropertyDescriptor(target, key: string) {
@@ -178,7 +184,8 @@ export function getPropsProxyHandlers(
         return {
           configurable: true,
           enumerable: true,
-          get: () => getAttrValue(target.rawProps, key),
+          get: () =>
+            once ? getOnceAttr(target, key) : getAttr(target.rawProps, key),
         }
       }
     },