]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-dom): prevent unnecessary DOM update from v-model (#11656)
authorHanse Kim <procrustes5@gmail.com>
Tue, 3 Sep 2024 09:44:07 +0000 (18:44 +0900)
committerGitHub <noreply@github.com>
Tue, 3 Sep 2024 09:44:07 +0000 (17:44 +0800)
close #11647

packages/runtime-dom/__tests__/directives/vModel.spec.ts
packages/runtime-dom/src/directives/vModel.ts

index 5ff0fd3521f8c1f6469908f63449f7e19ac6b9c9..534e5dfe98e4d0b47eec15883d8ab3981a78d0ea 100644 (file)
@@ -729,6 +729,120 @@ describe('vModel', () => {
     expect(bar.checked).toEqual(false)
   })
 
+  it('should not update DOM unnecessarily', async () => {
+    const component = defineComponent({
+      data() {
+        return { value: true }
+      },
+      render() {
+        return [
+          withVModel(
+            h('input', {
+              type: 'checkbox',
+              'onUpdate:modelValue': setValue.bind(this),
+            }),
+            this.value,
+          ),
+        ]
+      },
+    })
+    render(h(component), root)
+
+    const input = root.querySelector('input')
+    const data = root._vnode.component.data
+
+    const setCheckedSpy = vi.spyOn(input, 'checked', 'set')
+
+    // Trigger a change event without actually changing the value
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual(true)
+    expect(setCheckedSpy).not.toHaveBeenCalled()
+
+    // Change the value and trigger a change event
+    input.checked = false
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toEqual(false)
+    expect(setCheckedSpy).toHaveBeenCalledTimes(1)
+
+    setCheckedSpy.mockClear()
+
+    data.value = false
+    await nextTick()
+    expect(input.checked).toEqual(false)
+    expect(setCheckedSpy).not.toHaveBeenCalled()
+
+    data.value = true
+    await nextTick()
+    expect(input.checked).toEqual(true)
+    expect(setCheckedSpy).toHaveBeenCalledTimes(1)
+  })
+
+  it('should handle array values correctly without unnecessary updates', async () => {
+    const component = defineComponent({
+      data() {
+        return { value: ['foo'] }
+      },
+      render() {
+        return [
+          withVModel(
+            h('input', {
+              type: 'checkbox',
+              value: 'foo',
+              'onUpdate:modelValue': setValue.bind(this),
+            }),
+            this.value,
+          ),
+          withVModel(
+            h('input', {
+              type: 'checkbox',
+              value: 'bar',
+              'onUpdate:modelValue': setValue.bind(this),
+            }),
+            this.value,
+          ),
+        ]
+      },
+    })
+    render(h(component), root)
+
+    const [foo, bar] = root.querySelectorAll('input')
+    const data = root._vnode.component.data
+
+    const setCheckedSpyFoo = vi.spyOn(foo, 'checked', 'set')
+    const setCheckedSpyBar = vi.spyOn(bar, 'checked', 'set')
+
+    expect(foo.checked).toEqual(true)
+    expect(bar.checked).toEqual(false)
+
+    triggerEvent('change', foo)
+    await nextTick()
+    expect(data.value).toEqual(['foo'])
+    expect(setCheckedSpyFoo).not.toHaveBeenCalled()
+
+    bar.checked = true
+    triggerEvent('change', bar)
+    await nextTick()
+    expect(data.value).toEqual(['foo', 'bar'])
+    expect(setCheckedSpyBar).toHaveBeenCalledTimes(1)
+
+    setCheckedSpyFoo.mockClear()
+    setCheckedSpyBar.mockClear()
+
+    data.value = ['foo', 'bar']
+    await nextTick()
+    expect(setCheckedSpyFoo).not.toHaveBeenCalled()
+    expect(setCheckedSpyBar).not.toHaveBeenCalled()
+
+    data.value = ['bar']
+    await nextTick()
+    expect(setCheckedSpyFoo).toHaveBeenCalledTimes(1)
+    expect(setCheckedSpyBar).not.toHaveBeenCalled()
+    expect(foo.checked).toEqual(false)
+    expect(bar.checked).toEqual(true)
+  })
+
   it('should work with radio', async () => {
     const component = defineComponent({
       data() {
index 49f345fdebefe96331feec15dffce4145ea48070..b3d127fbe1a06c9b679cfac9af095ac3930a915d 100644 (file)
@@ -166,12 +166,19 @@ function setChecked(
   // store the v-model value on the element so it can be accessed by the
   // change listener.
   ;(el as any)._modelValue = value
+  let checked: boolean
+
   if (isArray(value)) {
-    el.checked = looseIndexOf(value, vnode.props!.value) > -1
+    checked = looseIndexOf(value, vnode.props!.value) > -1
   } else if (isSet(value)) {
-    el.checked = value.has(vnode.props!.value)
-  } else if (value !== oldValue) {
-    el.checked = looseEqual(value, getCheckboxValue(el, true))
+    checked = value.has(vnode.props!.value)
+  } else {
+    checked = looseEqual(value, getCheckboxValue(el, true))
+  }
+
+  // Only update if the checked state has changed
+  if (el.checked !== checked) {
+    el.checked = checked
   }
 }