Просмотр исходного кода

perf(runtime-vapor): stabilize unchanged dynamic props sources

daiwei 1 месяц назад
Родитель
Сommit
149746cf4d

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

@@ -669,6 +669,50 @@ describe('component: props', () => {
       expect(sourceCallCount).toBe(2)
     })
 
+    test('v-bind object should not update child when resolved values are unchanged', async () => {
+      let childRenderCount = 0
+      const activeId = ref(0)
+
+      const t0 = template('<div></div>', true)
+      const Child = defineVaporComponent({
+        props: ['active', 'tone'],
+        setup(props: any) {
+          const n0 = t0()
+          renderEffect(() => {
+            childRenderCount++
+            setElementText(n0, `${props.active}-${props.tone}`)
+          })
+          return n0
+        },
+      })
+
+      const { host } = define({
+        setup() {
+          return createComponent(Child, {
+            $: [
+              () => {
+                const active = activeId.value === 1
+                return {
+                  active,
+                  tone: 'stable',
+                  class: active ? 'active' : 'inactive',
+                }
+              },
+            ],
+          })
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('<div class="inactive">false-stable</div>')
+      expect(childRenderCount).toBe(1)
+
+      activeId.value = 2
+      await nextTick()
+
+      expect(host.innerHTML).toBe('<div class="inactive">false-stable</div>')
+      expect(childRenderCount).toBe(1)
+    })
+
     test('v-bind object should be cached when child accesses multiple attrs', () => {
       let sourceCallCount = 0
       const obj = ref({ foo: 1, bar: 2, baz: 3 })

+ 30 - 2
packages/runtime-vapor/src/componentProps.ts

@@ -5,6 +5,7 @@ import {
   hasOwn,
   isArray,
   isFunction,
+  isPlainObject,
   isString,
 } from '@vue/shared'
 import type { VaporComponent, VaporComponentInstance } from './component'
@@ -62,10 +63,10 @@ export function resolveFunctionSource<T>(
   // where source was defined.
   const parent = currentInstance && currentInstance.parent
   if (parent) {
-    source._cache = computed(() => {
+    source._cache = computed(oldValue => {
       const prev = setCurrentInstance(parent)
       try {
-        return source()
+        return stabilizeDynamicSourceValue(oldValue, source())
       } finally {
         setCurrentInstance(...prev)
       }
@@ -78,6 +79,33 @@ export function resolveFunctionSource<T>(
   return source()
 }
 
+function stabilizeDynamicSourceValue<T>(oldValue: T | undefined, value: T): T {
+  if (!isPlainObject(oldValue) || !isPlainObject(value)) {
+    return value
+  }
+
+  // Dynamic sources often allocate a fresh object even when the resolved
+  // props/attrs are unchanged. Keep the previous identity in that case so
+  // computed consumers do not trigger child updates for equivalent values.
+  const oldKeys = Object.keys(oldValue)
+  const newKeys = Object.keys(value)
+  if (oldKeys.length !== newKeys.length) {
+    return value
+  }
+
+  for (let i = 0; i < newKeys.length; i++) {
+    const key = newKeys[i]
+    if (
+      !hasOwn(oldValue, key) ||
+      !Object.is((oldValue as any)[key], (value as any)[key])
+    ) {
+      return value
+    }
+  }
+
+  return oldValue
+}
+
 export function getPropsProxyHandlers(
   comp: VaporComponent,
   once?: boolean,