diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index dcde7f488fe..70eac29764a 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -13,6 +13,7 @@ import { const normalizeEvent = cached((name: string): { name: string, + plain: boolean, once: boolean, capture: boolean, passive: boolean, @@ -25,8 +26,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 @@ -50,6 +53,11 @@ export function createFnInvoker (fns: Function | Array, vm: ?Component return invoker } +// #6552, #11925 +function prioritizePlainEvents (a, b) { + return a.plain ? -1 : b.plain ? 1 : 0 +} + export function updateListeners ( on: Object, oldOn: Object, @@ -59,6 +67,8 @@ export function updateListeners ( vm: Component ) { let name, def, cur, old, event + const toAdd = [] + let hasModifier = false for (name in on) { def = cur = on[name] old = oldOn[name] @@ -68,6 +78,7 @@ export function updateListeners ( cur = def.handler event.params = def.params } + if (!event.plain) hasModifier = true if (isUndef(cur)) { process.env.NODE_ENV !== 'production' && warn( `Invalid handler for event "${event.name}": got ` + String(cur), @@ -80,12 +91,20 @@ export function updateListeners ( if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } - add(event.name, cur, event.capture, event.passive, event.params) + 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.capture, event.passive, event.params) + } + } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name) diff --git a/test/unit/features/directives/model-text.spec.js b/test/unit/features/directives/model-text.spec.js index 7cf5167461f..b387d0aa39a 100644 --- a/test/unit/features/directives/model-text.spec.js +++ b/test/unit/features/directives/model-text.spec.js @@ -380,37 +380,68 @@ describe('Directive v-model text', () => { }) // #6552 - // This was original introduced due to the microtask between DOM events issue - // but fixed after switching to MessageChannel. + // 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 + // #11925 + // After fix #11925, input event should trigger an update after two listener, + // not after the first listener, before the second listener. + // So we should also test whether the v-model listener is triggered + // before the listener with once modifier by the variable vModelTriggerBeforeOnce. it('should not block input when another input listener with modifier is used', done => { + let vModelTriggerBeforeOnce = false + const captureInputValue = 'b' + const onceInputValue = 'x' const vm = new Vue({ data: { a: 'a', - foo: false + foo: false, + b: 'b', + bar: false, }, template: ` -
- {{ a }} -
foo
-
- `, +
+ {{ a }} +
foo
+ {{ b }} +
bar
+
+ `, methods: { onInput (e) { this.foo = true + }, + onInputBar (e) { + vModelTriggerBeforeOnce = this.b === onceInputValue + this.bar = true } } }).$mount() document.body.appendChild(vm.$el) vm.$refs.input.focus() - vm.$refs.input.value = 'b' + vm.$refs.input.value = captureInputValue triggerEvent(vm.$refs.input, 'input') + vm.$refs.onceInput.focus() + vm.$refs.onceInput.value = onceInputValue + triggerEvent(vm.$refs.onceInput, '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') + expect(vm.a).toBe(captureInputValue) + expect(vm.foo).toBe(true) + expect(vm.$refs.input.value).toBe(captureInputValue) + + expect(vm.b).toBe(onceInputValue) + expect(vm.bar).toBe(true) + expect(vm.$refs.onceInput.value).toBe(onceInputValue) + expect(vModelTriggerBeforeOnce).toBe(true) done() }, 16) })