]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(v-model): optimize v-model multiple select w/ large lists
authorEvan You <yyx990803@gmail.com>
Thu, 18 Jan 2024 11:55:15 +0000 (19:55 +0800)
committerEvan You <yyx990803@gmail.com>
Thu, 18 Jan 2024 11:55:15 +0000 (19:55 +0800)
close #10014

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

index 05ee59d90108fbd11b2349f6a19e376a2fc9c8d0..6cc7b53e2c23b17c2d5cf415475bdd261cdefbc6 100644 (file)
@@ -1037,15 +1037,25 @@ describe('vModel', () => {
     await nextTick()
     expect(data.value).toMatchObject([fooValue, barValue])
 
+    // reset
     foo.selected = false
     bar.selected = false
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toMatchObject([])
+
     data.value = [fooValue, barValue]
     await nextTick()
     expect(foo.selected).toEqual(true)
     expect(bar.selected).toEqual(true)
 
+    // reset
     foo.selected = false
     bar.selected = false
+    triggerEvent('change', input)
+    await nextTick()
+    expect(data.value).toMatchObject([])
+
     data.value = [{ foo: 1 }, { bar: 1 }]
     await nextTick()
     // looseEqual
index 7b387096b6804c520692e9e57d9190b89fd69951..c581cb10589edaac8ca3586fa536df950ba26429 100644 (file)
@@ -3,6 +3,7 @@ import {
   type DirectiveHook,
   type ObjectDirective,
   type VNode,
+  nextTick,
   warn,
 } from '@vue/runtime-core'
 import { addEventListener } from '../modules/events'
@@ -38,7 +39,9 @@ function onCompositionEnd(e: Event) {
 
 const assignKey = Symbol('_assign')
 
-type ModelDirective<T> = ObjectDirective<T & { [assignKey]: AssignerFn }>
+type ModelDirective<T> = ObjectDirective<
+  T & { [assignKey]: AssignerFn; _assigning?: boolean }
+>
 
 // We are exporting the v-model runtime directly as vnode hooks so that it can
 // be tree-shaken in case v-model is never used.
@@ -197,25 +200,37 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
             : selectedVal
           : selectedVal[0],
       )
+      el._assigning = true
+      nextTick(() => {
+        el._assigning = false
+      })
     })
     el[assignKey] = getModelAssigner(vnode)
   },
   // set value in mounted & updated because <select> relies on its children
   // <option>s.
-  mounted(el, { value }) {
-    setSelected(el, value)
+  mounted(el, { value, oldValue, modifiers: { number } }) {
+    setSelected(el, value, oldValue, number)
   },
   beforeUpdate(el, _binding, vnode) {
     el[assignKey] = getModelAssigner(vnode)
   },
-  updated(el, { value }) {
-    setSelected(el, value)
+  updated(el, { value, oldValue, modifiers: { number } }) {
+    if (!el._assigning) {
+      setSelected(el, value, oldValue, number)
+    }
   },
 }
 
-function setSelected(el: HTMLSelectElement, value: any) {
+function setSelected(
+  el: HTMLSelectElement,
+  value: any,
+  oldValue: any,
+  number: boolean,
+) {
   const isMultiple = el.multiple
-  if (isMultiple && !isArray(value) && !isSet(value)) {
+  const isArrayValue = isArray(value)
+  if (isMultiple && !isArrayValue && !isSet(value)) {
     __DEV__ &&
       warn(
         `<select multiple v-model> expects an Array or Set value for its binding, ` +
@@ -223,12 +238,26 @@ function setSelected(el: HTMLSelectElement, value: any) {
       )
     return
   }
+
+  // fast path for updates triggered by other changes
+  if (isArrayValue && looseEqual(value, oldValue)) {
+    return
+  }
+
   for (let i = 0, l = el.options.length; i < l; i++) {
     const option = el.options[i]
     const optionValue = getValue(option)
     if (isMultiple) {
-      if (isArray(value)) {
-        option.selected = looseIndexOf(value, optionValue) > -1
+      if (isArrayValue) {
+        const optionType = typeof optionValue
+        // fast path for string / number values
+        if (optionType === 'string' || optionType === 'number') {
+          option.selected = value.includes(
+            number ? looseToNumber(optionValue) : optionValue,
+          )
+        } else {
+          option.selected = looseIndexOf(value, optionValue) > -1
+        }
       } else {
         option.selected = value.has(optionValue)
       }