Pārlūkot izejas kodu

fix(runtime-core): prevent child component updates when style remains unchanged (#12825)

close #12826
edison 2 mēneši atpakaļ
vecāks
revīzija
57866b5af1

+ 52 - 0
packages/runtime-core/__tests__/rendererComponent.spec.ts

@@ -6,6 +6,7 @@ import {
   inject,
   nextTick,
   nodeOps,
+  onMounted,
   provide,
   ref,
   render,
@@ -474,4 +475,55 @@ describe('renderer: component', () => {
       `Property '$attrs' was accessed via 'this'. Avoid using 'this' in templates.`,
     ).toHaveBeenWarned()
   })
+
+  test('should not update child component if style is not changed', async () => {
+    const text = ref(0)
+    const spy = vi.fn()
+
+    const ClientOnly = {
+      setup(_: any, { slots }: SetupContext) {
+        const mounted = ref(false)
+        onMounted(() => {
+          mounted.value = true
+        })
+        return () => {
+          if (mounted.value) {
+            return slots.default!()
+          }
+        }
+      },
+    }
+
+    const App = {
+      render() {
+        return h(ClientOnly, null, {
+          default: () => [
+            h('span', null, [text.value]),
+            h(Comp, { style: { width: '100%' } }),
+          ],
+        })
+      },
+    }
+
+    const Comp = {
+      render(this: any) {
+        spy()
+        return null
+      },
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(App), root)
+    expect(serializeInner(root)).toBe(`<!---->`)
+    await nextTick()
+
+    expect(serializeInner(root)).toBe(`<span>0</span><!---->`)
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    text.value++
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<span>1</span><!---->`)
+    // expect Comp to not be re-rendered
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
 })

+ 23 - 3
packages/runtime-core/src/componentRenderUtils.ts

@@ -15,7 +15,14 @@ import {
   normalizeVNode,
 } from './vnode'
 import { ErrorCodes, handleError } from './errorHandling'
-import { PatchFlags, ShapeFlags, isModelListener, isOn } from '@vue/shared'
+import {
+  PatchFlags,
+  ShapeFlags,
+  isModelListener,
+  isObject,
+  isOn,
+  looseEqual,
+} from '@vue/shared'
 import { warn } from './warning'
 import { isHmrUpdating } from './hmr'
 import type { NormalizedProps } from './componentProps'
@@ -399,7 +406,7 @@ export function shouldUpdateComponent(
       for (let i = 0; i < dynamicProps.length; i++) {
         const key = dynamicProps[i]
         if (
-          nextProps![key] !== prevProps![key] &&
+          hasPropValueChanged(nextProps!, prevProps!, key) &&
           !isEmitListener(emits, key)
         ) {
           return true
@@ -441,7 +448,7 @@ function hasPropsChanged(
   for (let i = 0; i < nextKeys.length; i++) {
     const key = nextKeys[i]
     if (
-      nextProps[key] !== prevProps[key] &&
+      hasPropValueChanged(nextProps, prevProps, key) &&
       !isEmitListener(emitsOptions, key)
     ) {
       return true
@@ -450,6 +457,19 @@ function hasPropsChanged(
   return false
 }
 
+function hasPropValueChanged(
+  nextProps: Data,
+  prevProps: Data,
+  key: string,
+): boolean {
+  const nextProp = nextProps[key]
+  const prevProp = prevProps[key]
+  if (key === 'style' && isObject(nextProp) && isObject(prevProp)) {
+    return !looseEqual(nextProp, prevProp)
+  }
+  return nextProp !== prevProp
+}
+
 export function updateHOCHostEl(
   { vnode, parent }: ComponentInternalInstance,
   el: typeof vnode.el, // HostNode