Ver código fonte

perf(v-model): optimize v-model multiple select w/ large lists

close #10014
Evan You 2 anos atrás
pai
commit
2ffb956efe

+ 10 - 0
packages/runtime-dom/__tests__/directives/vModel.spec.ts

@@ -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

+ 38 - 9
packages/runtime-dom/src/directives/vModel.ts

@@ -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)
       }