Преглед изворни кода

perf(runtime-vapor): cache dynamic prop/attr and slot function sources using computed to prevent redundant calls. (#14178)

edison пре 4 месеци
родитељ
комит
5ee8913140

+ 153 - 0
packages/runtime-vapor/__tests__/componentProps.spec.ts

@@ -591,4 +591,157 @@ describe('component: props', () => {
     render({ msg: () => 'test' })
     render({ msg: () => 'test' })
     expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
     expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
   })
   })
+
+  describe('dynamic props source caching', () => {
+    test('v-bind object should be cached when child accesses multiple props', () => {
+      let sourceCallCount = 0
+      const obj = ref({ foo: 1, bar: 2, baz: 3 })
+
+      const t0 = template('<div></div>')
+      const Child = defineVaporComponent({
+        props: ['foo', 'bar', 'baz'],
+        setup(props: any) {
+          const n0 = t0()
+          // Child component accesses multiple props
+          renderEffect(() => {
+            setElementText(n0, `${props.foo}-${props.bar}-${props.baz}`)
+          })
+          return n0
+        },
+      })
+
+      const { host } = define({
+        setup() {
+          return createComponent(Child, {
+            $: [
+              () => {
+                sourceCallCount++
+                return obj.value
+              },
+            ],
+          })
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('<div>1-2-3</div>')
+      // Source should only be called once even though 3 props are accessed
+      expect(sourceCallCount).toBe(1)
+    })
+
+    test('v-bind object should update when source changes', async () => {
+      let sourceCallCount = 0
+      const obj = ref({ foo: 1, bar: 2 })
+
+      const t0 = template('<div></div>')
+      const Child = defineVaporComponent({
+        props: ['foo', 'bar'],
+        setup(props: any) {
+          const n0 = t0()
+          renderEffect(() => {
+            setElementText(n0, `${props.foo}-${props.bar}`)
+          })
+          return n0
+        },
+      })
+
+      const { host } = define({
+        setup() {
+          return createComponent(Child, {
+            $: [
+              () => {
+                sourceCallCount++
+                return obj.value
+              },
+            ],
+          })
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('<div>1-2</div>')
+      expect(sourceCallCount).toBe(1)
+
+      // Update source
+      obj.value = { foo: 10, bar: 20 }
+      await nextTick()
+
+      expect(host.innerHTML).toBe('<div>10-20</div>')
+      // Should be called again after source changes
+      expect(sourceCallCount).toBe(2)
+    })
+
+    test('v-bind object should be cached when child accesses multiple attrs', () => {
+      let sourceCallCount = 0
+      const obj = ref({ foo: 1, bar: 2, baz: 3 })
+
+      const t0 = template('<div></div>')
+      const Child = defineVaporComponent({
+        // No props declaration - all become attrs
+        setup(_: any, { attrs }: any) {
+          const n0 = t0()
+          renderEffect(() => {
+            setElementText(n0, `${attrs.foo}-${attrs.bar}-${attrs.baz}`)
+          })
+          return n0
+        },
+      })
+
+      const { host } = define({
+        setup() {
+          return createComponent(Child, {
+            $: [
+              () => {
+                sourceCallCount++
+                return obj.value
+              },
+            ],
+          })
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('<div foo="1" bar="2" baz="3">1-2-3</div>')
+      // Source should only be called once
+      expect(sourceCallCount).toBe(1)
+    })
+
+    test('mixed static and dynamic props', async () => {
+      let sourceCallCount = 0
+      const obj = ref({ foo: 1 })
+
+      const t0 = template('<div></div>')
+      const Child = defineVaporComponent({
+        props: ['id', 'foo', 'class'],
+        setup(props: any) {
+          const n0 = t0()
+          renderEffect(() => {
+            setElementText(n0, `${props.id}-${props.foo}-${props.class}`)
+          })
+          return n0
+        },
+      })
+
+      const { host } = define({
+        setup() {
+          return createComponent(Child, {
+            id: () => 'static',
+            $: [
+              () => {
+                sourceCallCount++
+                return obj.value
+              },
+              { class: () => 'bar' },
+            ],
+          })
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('<div>static-1-bar</div>')
+      expect(sourceCallCount).toBe(1)
+
+      obj.value = { foo: 2 }
+      await nextTick()
+
+      expect(host.innerHTML).toBe('<div>static-2-bar</div>')
+      expect(sourceCallCount).toBe(2)
+    })
+  })
 })
 })

+ 27 - 6
packages/runtime-vapor/src/componentProps.ts

@@ -20,7 +20,7 @@ import {
   validateProps,
   validateProps,
   warn,
   warn,
 } from '@vue/runtime-dom'
 } from '@vue/runtime-dom'
-import { ReactiveFlags } from '@vue/reactivity'
+import { type ComputedRef, ReactiveFlags, computed } from '@vue/reactivity'
 import { normalizeEmitsOptions } from './componentEmits'
 import { normalizeEmitsOptions } from './componentEmits'
 import { renderEffect } from './renderEffect'
 import { renderEffect } from './renderEffect'
 import { pauseTracking, resetTracking } from '@vue/reactivity'
 import { pauseTracking, resetTracking } from '@vue/reactivity'
@@ -35,11 +35,24 @@ export type DynamicPropsSource =
   | (() => Record<string, unknown>)
   | (() => Record<string, unknown>)
   | Record<string, () => unknown>
   | Record<string, () => unknown>
 
 
-// TODO optimization: maybe convert functions into computeds
 export function resolveSource(
 export function resolveSource(
   source: Record<string, any> | (() => Record<string, any>),
   source: Record<string, any> | (() => Record<string, any>),
 ): Record<string, any> {
 ): Record<string, any> {
-  return isFunction(source) ? source() : source
+  return isFunction(source)
+    ? resolveFunctionSource(source as () => Record<string, any>)
+    : source
+}
+
+/**
+ * Resolve a function source with computed caching.
+ */
+export function resolveFunctionSource<T>(
+  source: (() => T) & { _cache?: ComputedRef<T> },
+): T {
+  if (!source._cache) {
+    source._cache = computed(source)
+  }
+  return source._cache.value
 }
 }
 
 
 export function getPropsProxyHandlers(
 export function getPropsProxyHandlers(
@@ -78,7 +91,11 @@ export function getPropsProxyHandlers(
       while (i--) {
       while (i--) {
         source = dynamicSources[i]
         source = dynamicSources[i]
         isDynamic = isFunction(source)
         isDynamic = isFunction(source)
-        source = isDynamic ? (source as Function)() : source
+        source = isDynamic
+          ? (resolveFunctionSource(
+              source as () => Record<string, unknown>,
+            ) as any)
+          : source
         for (rawKey in source) {
         for (rawKey in source) {
           if (camelize(rawKey) === key) {
           if (camelize(rawKey) === key) {
             return resolvePropValue(
             return resolvePropValue(
@@ -205,7 +222,11 @@ export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
     while (i--) {
     while (i--) {
       source = dynamicSources[i]
       source = dynamicSources[i]
       isDynamic = isFunction(source)
       isDynamic = isFunction(source)
-      source = isDynamic ? (source as Function)() : source
+      source = isDynamic
+        ? (resolveFunctionSource(
+            source as () => Record<string, unknown>,
+          ) as any)
+        : source
       if (source && hasOwn(source, key)) {
       if (source && hasOwn(source, key)) {
         const value = isDynamic ? source[key] : source[key]()
         const value = isDynamic ? source[key] : source[key]()
         if (merged) {
         if (merged) {
@@ -337,7 +358,7 @@ export function resolveDynamicProps(props: RawProps): Record<string, unknown> {
   if (props.$) {
   if (props.$) {
     for (const source of props.$) {
     for (const source of props.$) {
       const isDynamic = isFunction(source)
       const isDynamic = isFunction(source)
-      const resolved = isDynamic ? source() : source
+      const resolved = isDynamic ? resolveFunctionSource(source) : source
       for (const key in resolved) {
       for (const key in resolved) {
         const value = isDynamic ? resolved[key] : (resolved[key] as Function)()
         const value = isDynamic ? resolved[key] : (resolved[key] as Function)()
         if (key === 'class' || key === 'style') {
         if (key === 'class' || key === 'style') {

+ 4 - 20
packages/runtime-vapor/src/componentSlots.ts

@@ -1,7 +1,6 @@
 import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
 import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
-import { type ComputedRef, computed } from '@vue/reactivity'
 import { type Block, type BlockFn, insert, setScopeId } from './block'
 import { type Block, type BlockFn, insert, setScopeId } from './block'
-import { rawPropsProxyHandlers } from './componentProps'
+import { rawPropsProxyHandlers, resolveFunctionSource } from './componentProps'
 import {
 import {
   type GenericComponentInstance,
   type GenericComponentInstance,
   currentInstance,
   currentInstance,
@@ -52,24 +51,9 @@ export type StaticSlots = Record<string, VaporSlot>
 
 
 export type VaporSlot = BlockFn
 export type VaporSlot = BlockFn
 export type DynamicSlot = { name: string; fn: VaporSlot }
 export type DynamicSlot = { name: string; fn: VaporSlot }
-export type DynamicSlotFn = (() => DynamicSlot | DynamicSlot[]) & {
-  _cache?: ComputedRef<DynamicSlot | DynamicSlot[]>
-}
+export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
 export type DynamicSlotSource = StaticSlots | DynamicSlotFn
 export type DynamicSlotSource = StaticSlots | DynamicSlotFn
 
 
-/**
- * Get cached result of a DynamicSlotFn.
- * Uses computed to cache the result and avoid redundant calls.
- */
-function resolveDynamicSlot(
-  source: DynamicSlotFn,
-): DynamicSlot | DynamicSlot[] {
-  if (!source._cache) {
-    source._cache = computed(source)
-  }
-  return source._cache.value
-}
-
 export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
 export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
   get: getSlot,
   get: getSlot,
   has: (target, key: string) => !!getSlot(target, key),
   has: (target, key: string) => !!getSlot(target, key),
@@ -90,7 +74,7 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
       keys = keys.filter(k => k !== '$')
       keys = keys.filter(k => k !== '$')
       for (const source of dynamicSources) {
       for (const source of dynamicSources) {
         if (isFunction(source)) {
         if (isFunction(source)) {
-          const slot = resolveDynamicSlot(source)
+          const slot = resolveFunctionSource(source)
           if (isArray(slot)) {
           if (isArray(slot)) {
             for (const s of slot) keys.push(String(s.name))
             for (const s of slot) keys.push(String(s.name))
           } else {
           } else {
@@ -119,7 +103,7 @@ export function getSlot(
     while (i--) {
     while (i--) {
       source = dynamicSources[i]
       source = dynamicSources[i]
       if (isFunction(source)) {
       if (isFunction(source)) {
-        const slot = resolveDynamicSlot(source)
+        const slot = resolveFunctionSource(source)
         if (slot) {
         if (slot) {
           if (isArray(slot)) {
           if (isArray(slot)) {
             for (const s of slot) {
             for (const s of slot) {