Просмотр исходного кода

fix(v-model): fix input listener with modifier blocking v-model update

fix #6552
Evan You 8 лет назад
Родитель
Сommit
6f312d636c

+ 22 - 2
src/core/vdom/helpers/update-listeners.js

@@ -5,9 +5,11 @@ import { cached, isUndef } from 'shared/util'
 
 const normalizeEvent = cached((name: string): {
   name: string,
+  plain: boolean,
   once: boolean,
   capture: boolean,
-  passive: boolean
+  passive: boolean,
+  handler?: Function
 } => {
   const passive = name.charAt(0) === '&'
   name = passive ? name.slice(1) : name
@@ -15,8 +17,10 @@ const normalizeEvent = cached((name: string): {
   name = once ? name.slice(1) : name
   const capture = name.charAt(0) === '!'
   name = capture ? name.slice(1) : name
+  const plain = !(passive || once || capture)
   return {
     name,
+    plain,
     once,
     capture,
     passive
@@ -40,6 +44,11 @@ export function createFnInvoker (fns: Function | Array<Function>): Function {
   return invoker
 }
 
+// #6552
+function prioritizePlainEvents (a, b) {
+  return a.plain ? -1 : b.plain ? 1 : 0
+}
+
 export function updateListeners (
   on: Object,
   oldOn: Object,
@@ -48,10 +57,13 @@ export function updateListeners (
   vm: Component
 ) {
   let name, cur, old, event
+  const toAdd = []
+  let hasModifier = false
   for (name in on) {
     cur = on[name]
     old = oldOn[name]
     event = normalizeEvent(name)
+    if (!event.plain) hasModifier = true
     if (isUndef(cur)) {
       process.env.NODE_ENV !== 'production' && warn(
         `Invalid handler for event "${event.name}": got ` + String(cur),
@@ -61,12 +73,20 @@ export function updateListeners (
       if (isUndef(cur.fns)) {
         cur = on[name] = createFnInvoker(cur)
       }
-      add(event.name, cur, event.once, event.capture, event.passive)
+      event.handler = cur
+      toAdd.push(event)
     } else if (cur !== old) {
       old.fns = cur
       on[name] = old
     }
   }
+  if (toAdd.length) {
+    if (hasModifier) toAdd.sort(prioritizePlainEvents)
+    for (let i = 0; i < toAdd.length; i++) {
+      const event = toAdd[i]
+      add(event.name, event.handler, event.once, event.capture, event.passive)
+    }
+  }
   for (name in oldOn) {
     if (isUndef(on[name])) {
       event = normalizeEvent(name)

+ 61 - 0
test/unit/features/directives/model-text.spec.js

@@ -293,5 +293,66 @@ describe('Directive v-model text', () => {
       triggerEvent(vm.$el, 'compositionend')
       expect(spy.calls.count()).toBe(2)
     })
+
+    // #4392
+    it('should not update value with modifiers when in focus if post-conversion values are the same', done => {
+      const vm = new Vue({
+        data: {
+          a: 1,
+          foo: false
+        },
+        template: '<div>{{ foo }}<input ref="input" v-model.number="a"></div>'
+      }).$mount()
+
+      document.body.appendChild(vm.$el)
+      vm.$refs.input.focus()
+      vm.$refs.input.value = '1.000'
+      vm.foo = true
+
+      waitForUpdate(() => {
+        expect(vm.$refs.input.value).toBe('1.000')
+      }).then(done)
+    })
+
+    // #6552
+    // Root cause: input listeners with modifiers are added as a separate
+    // DOM listener. If a change is triggered inside this listener, an update
+    // will happen before the second listener is fired! (obviously microtasks
+    // can be processed in between DOM events on the same element)
+    // This causes the domProps module to receive state that has not been
+    // updated by v-model yet (because v-model's listener has not fired yet)
+    // Solution: make sure to always fire v-model's listener first
+    it('should not block input when another input listener with modifier is used', done => {
+      const vm = new Vue({
+        data: {
+          a: 'a',
+          foo: false
+        },
+        template: `
+          <div>
+            <input ref="input" v-model="a" @input.capture="onInput">{{ a }}
+            <div v-if="foo">foo</div>
+          </div>
+        `,
+        methods: {
+          onInput (e) {
+            this.foo = true
+          }
+        }
+      }).$mount()
+
+      document.body.appendChild(vm.$el)
+      vm.$refs.input.focus()
+      vm.$refs.input.value = 'b'
+      triggerEvent(vm.$refs.input, 'input')
+
+      // not using wait for update here because there will be two update cycles
+      // one caused by onInput in the first listener
+      setTimeout(() => {
+        expect(vm.a).toBe('b')
+        expect(vm.$refs.input.value).toBe('b')
+        done()
+      }, 16)
+    })
   }
 })