From 0629e969bac56583f9bc8548f90cb37c6de4fa78 Mon Sep 17 00:00:00 2001 From: juniortour Date: Sun, 14 Mar 2021 11:43:35 +0800 Subject: [PATCH] fix(v-model): ensure v-model listener trigger before listener with modifier This reverts commit 62405aa9035d5f547c0440263f16f21c1325f100. Because after v2.6.0, nextTick always use microtask. There will be an update between 2 listener and block the v-model listener to update. We need to ensure the listener of v-model is triggered before listener with modifier to fix #11925. --- src/core/vdom/helpers/update-listeners.js | 21 +++++++- .../features/directives/model-text.spec.js | 53 +++++++++++++++---- 2 files changed, 62 insertions(+), 12 deletions(-) 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) })