diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts index 5ff0fd3521f..534e5dfe98e 100644 --- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts @@ -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() { diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 49f345fdebe..b3d127fbe1a 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -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 } }