Skip to content

Commit b1be9bd

Browse files
authored
fix(runtime-dom): prevent unnecessary DOM update from v-model (#11656)
close #11647
1 parent 6039e25 commit b1be9bd

File tree

2 files changed

+125
-4
lines changed

2 files changed

+125
-4
lines changed

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

+114
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,120 @@ describe('vModel', () => {
729729
expect(bar.checked).toEqual(false)
730730
})
731731

732+
it('should not update DOM unnecessarily', async () => {
733+
const component = defineComponent({
734+
data() {
735+
return { value: true }
736+
},
737+
render() {
738+
return [
739+
withVModel(
740+
h('input', {
741+
type: 'checkbox',
742+
'onUpdate:modelValue': setValue.bind(this),
743+
}),
744+
this.value,
745+
),
746+
]
747+
},
748+
})
749+
render(h(component), root)
750+
751+
const input = root.querySelector('input')
752+
const data = root._vnode.component.data
753+
754+
const setCheckedSpy = vi.spyOn(input, 'checked', 'set')
755+
756+
// Trigger a change event without actually changing the value
757+
triggerEvent('change', input)
758+
await nextTick()
759+
expect(data.value).toEqual(true)
760+
expect(setCheckedSpy).not.toHaveBeenCalled()
761+
762+
// Change the value and trigger a change event
763+
input.checked = false
764+
triggerEvent('change', input)
765+
await nextTick()
766+
expect(data.value).toEqual(false)
767+
expect(setCheckedSpy).toHaveBeenCalledTimes(1)
768+
769+
setCheckedSpy.mockClear()
770+
771+
data.value = false
772+
await nextTick()
773+
expect(input.checked).toEqual(false)
774+
expect(setCheckedSpy).not.toHaveBeenCalled()
775+
776+
data.value = true
777+
await nextTick()
778+
expect(input.checked).toEqual(true)
779+
expect(setCheckedSpy).toHaveBeenCalledTimes(1)
780+
})
781+
782+
it('should handle array values correctly without unnecessary updates', async () => {
783+
const component = defineComponent({
784+
data() {
785+
return { value: ['foo'] }
786+
},
787+
render() {
788+
return [
789+
withVModel(
790+
h('input', {
791+
type: 'checkbox',
792+
value: 'foo',
793+
'onUpdate:modelValue': setValue.bind(this),
794+
}),
795+
this.value,
796+
),
797+
withVModel(
798+
h('input', {
799+
type: 'checkbox',
800+
value: 'bar',
801+
'onUpdate:modelValue': setValue.bind(this),
802+
}),
803+
this.value,
804+
),
805+
]
806+
},
807+
})
808+
render(h(component), root)
809+
810+
const [foo, bar] = root.querySelectorAll('input')
811+
const data = root._vnode.component.data
812+
813+
const setCheckedSpyFoo = vi.spyOn(foo, 'checked', 'set')
814+
const setCheckedSpyBar = vi.spyOn(bar, 'checked', 'set')
815+
816+
expect(foo.checked).toEqual(true)
817+
expect(bar.checked).toEqual(false)
818+
819+
triggerEvent('change', foo)
820+
await nextTick()
821+
expect(data.value).toEqual(['foo'])
822+
expect(setCheckedSpyFoo).not.toHaveBeenCalled()
823+
824+
bar.checked = true
825+
triggerEvent('change', bar)
826+
await nextTick()
827+
expect(data.value).toEqual(['foo', 'bar'])
828+
expect(setCheckedSpyBar).toHaveBeenCalledTimes(1)
829+
830+
setCheckedSpyFoo.mockClear()
831+
setCheckedSpyBar.mockClear()
832+
833+
data.value = ['foo', 'bar']
834+
await nextTick()
835+
expect(setCheckedSpyFoo).not.toHaveBeenCalled()
836+
expect(setCheckedSpyBar).not.toHaveBeenCalled()
837+
838+
data.value = ['bar']
839+
await nextTick()
840+
expect(setCheckedSpyFoo).toHaveBeenCalledTimes(1)
841+
expect(setCheckedSpyBar).not.toHaveBeenCalled()
842+
expect(foo.checked).toEqual(false)
843+
expect(bar.checked).toEqual(true)
844+
})
845+
732846
it('should work with radio', async () => {
733847
const component = defineComponent({
734848
data() {

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,19 @@ function setChecked(
166166
// store the v-model value on the element so it can be accessed by the
167167
// change listener.
168168
;(el as any)._modelValue = value
169+
let checked: boolean
170+
169171
if (isArray(value)) {
170-
el.checked = looseIndexOf(value, vnode.props!.value) > -1
172+
checked = looseIndexOf(value, vnode.props!.value) > -1
171173
} else if (isSet(value)) {
172-
el.checked = value.has(vnode.props!.value)
173-
} else if (value !== oldValue) {
174-
el.checked = looseEqual(value, getCheckboxValue(el, true))
174+
checked = value.has(vnode.props!.value)
175+
} else {
176+
checked = looseEqual(value, getCheckboxValue(el, true))
177+
}
178+
179+
// Only update if the checked state has changed
180+
if (el.checked !== checked) {
181+
el.checked = checked
175182
}
176183
}
177184

0 commit comments

Comments
 (0)