Skip to content

Commit 2ffb956

Browse files
committed
perf(v-model): optimize v-model multiple select w/ large lists
close #10014
1 parent 75e866b commit 2ffb956

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,15 +1037,25 @@ describe('vModel', () => {
10371037
await nextTick()
10381038
expect(data.value).toMatchObject([fooValue, barValue])
10391039

1040+
// reset
10401041
foo.selected = false
10411042
bar.selected = false
1043+
triggerEvent('change', input)
1044+
await nextTick()
1045+
expect(data.value).toMatchObject([])
1046+
10421047
data.value = [fooValue, barValue]
10431048
await nextTick()
10441049
expect(foo.selected).toEqual(true)
10451050
expect(bar.selected).toEqual(true)
10461051

1052+
// reset
10471053
foo.selected = false
10481054
bar.selected = false
1055+
triggerEvent('change', input)
1056+
await nextTick()
1057+
expect(data.value).toMatchObject([])
1058+
10491059
data.value = [{ foo: 1 }, { bar: 1 }]
10501060
await nextTick()
10511061
// looseEqual

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type DirectiveHook,
44
type ObjectDirective,
55
type VNode,
6+
nextTick,
67
warn,
78
} from '@vue/runtime-core'
89
import { addEventListener } from '../modules/events'
@@ -38,7 +39,9 @@ function onCompositionEnd(e: Event) {
3839

3940
const assignKey = Symbol('_assign')
4041

41-
type ModelDirective<T> = ObjectDirective<T & { [assignKey]: AssignerFn }>
42+
type ModelDirective<T> = ObjectDirective<
43+
T & { [assignKey]: AssignerFn; _assigning?: boolean }
44+
>
4245

4346
// We are exporting the v-model runtime directly as vnode hooks so that it can
4447
// be tree-shaken in case v-model is never used.
@@ -197,38 +200,64 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
197200
: selectedVal
198201
: selectedVal[0],
199202
)
203+
el._assigning = true
204+
nextTick(() => {
205+
el._assigning = false
206+
})
200207
})
201208
el[assignKey] = getModelAssigner(vnode)
202209
},
203210
// set value in mounted & updated because <select> relies on its children
204211
// <option>s.
205-
mounted(el, { value }) {
206-
setSelected(el, value)
212+
mounted(el, { value, oldValue, modifiers: { number } }) {
213+
setSelected(el, value, oldValue, number)
207214
},
208215
beforeUpdate(el, _binding, vnode) {
209216
el[assignKey] = getModelAssigner(vnode)
210217
},
211-
updated(el, { value }) {
212-
setSelected(el, value)
218+
updated(el, { value, oldValue, modifiers: { number } }) {
219+
if (!el._assigning) {
220+
setSelected(el, value, oldValue, number)
221+
}
213222
},
214223
}
215224

216-
function setSelected(el: HTMLSelectElement, value: any) {
225+
function setSelected(
226+
el: HTMLSelectElement,
227+
value: any,
228+
oldValue: any,
229+
number: boolean,
230+
) {
217231
const isMultiple = el.multiple
218-
if (isMultiple && !isArray(value) && !isSet(value)) {
232+
const isArrayValue = isArray(value)
233+
if (isMultiple && !isArrayValue && !isSet(value)) {
219234
__DEV__ &&
220235
warn(
221236
`<select multiple v-model> expects an Array or Set value for its binding, ` +
222237
`but got ${Object.prototype.toString.call(value).slice(8, -1)}.`,
223238
)
224239
return
225240
}
241+
242+
// fast path for updates triggered by other changes
243+
if (isArrayValue && looseEqual(value, oldValue)) {
244+
return
245+
}
246+
226247
for (let i = 0, l = el.options.length; i < l; i++) {
227248
const option = el.options[i]
228249
const optionValue = getValue(option)
229250
if (isMultiple) {
230-
if (isArray(value)) {
231-
option.selected = looseIndexOf(value, optionValue) > -1
251+
if (isArrayValue) {
252+
const optionType = typeof optionValue
253+
// fast path for string / number values
254+
if (optionType === 'string' || optionType === 'number') {
255+
option.selected = value.includes(
256+
number ? looseToNumber(optionValue) : optionValue,
257+
)
258+
} else {
259+
option.selected = looseIndexOf(value, optionValue) > -1
260+
}
232261
} else {
233262
option.selected = value.has(optionValue)
234263
}

0 commit comments

Comments
 (0)