Skip to content

Commit 0629e96

Browse files
committed
fix(v-model): ensure v-model listener trigger before listener with modifier
This reverts commit 62405aa. 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.
1 parent e20581f commit 0629e96

File tree

2 files changed

+62
-12
lines changed

2 files changed

+62
-12
lines changed

src/core/vdom/helpers/update-listeners.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313

1414
const normalizeEvent = cached((name: string): {
1515
name: string,
16+
plain: boolean,
1617
once: boolean,
1718
capture: boolean,
1819
passive: boolean,
@@ -25,8 +26,10 @@ const normalizeEvent = cached((name: string): {
2526
name = once ? name.slice(1) : name
2627
const capture = name.charAt(0) === '!'
2728
name = capture ? name.slice(1) : name
29+
const plain = !(passive || once || capture)
2830
return {
2931
name,
32+
plain,
3033
once,
3134
capture,
3235
passive
@@ -50,6 +53,11 @@ export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component
5053
return invoker
5154
}
5255

56+
// #6552, #11925
57+
function prioritizePlainEvents (a, b) {
58+
return a.plain ? -1 : b.plain ? 1 : 0
59+
}
60+
5361
export function updateListeners (
5462
on: Object,
5563
oldOn: Object,
@@ -59,6 +67,8 @@ export function updateListeners (
5967
vm: Component
6068
) {
6169
let name, def, cur, old, event
70+
const toAdd = []
71+
let hasModifier = false
6272
for (name in on) {
6373
def = cur = on[name]
6474
old = oldOn[name]
@@ -68,6 +78,7 @@ export function updateListeners (
6878
cur = def.handler
6979
event.params = def.params
7080
}
81+
if (!event.plain) hasModifier = true
7182
if (isUndef(cur)) {
7283
process.env.NODE_ENV !== 'production' && warn(
7384
`Invalid handler for event "${event.name}": got ` + String(cur),
@@ -80,12 +91,20 @@ export function updateListeners (
8091
if (isTrue(event.once)) {
8192
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
8293
}
83-
add(event.name, cur, event.capture, event.passive, event.params)
94+
event.handler = cur
95+
toAdd.push(event)
8496
} else if (cur !== old) {
8597
old.fns = cur
8698
on[name] = old
8799
}
88100
}
101+
if (toAdd.length) {
102+
if (hasModifier) toAdd.sort(prioritizePlainEvents)
103+
for (let i = 0; i < toAdd.length; i++) {
104+
const event = toAdd[i]
105+
add(event.name, event.handler, event.capture, event.passive, event.params)
106+
}
107+
}
89108
for (name in oldOn) {
90109
if (isUndef(on[name])) {
91110
event = normalizeEvent(name)

test/unit/features/directives/model-text.spec.js

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -380,37 +380,68 @@ describe('Directive v-model text', () => {
380380
})
381381

382382
// #6552
383-
// This was original introduced due to the microtask between DOM events issue
384-
// but fixed after switching to MessageChannel.
383+
// Root cause: input listeners with modifiers are added as a separate
384+
// DOM listener. If a change is triggered inside this listener, an update
385+
// will happen before the second listener is fired! (obviously microtasks
386+
// can be processed in between DOM events on the same element)
387+
// This causes the domProps module to receive state that has not been
388+
// updated by v-model yet (because v-model's listener has not fired yet)
389+
// Solution: make sure to always fire v-model's listener first
390+
// #11925
391+
// After fix #11925, input event should trigger an update after two listener,
392+
// not after the first listener, before the second listener.
393+
// So we should also test whether the v-model listener is triggered
394+
// before the listener with once modifier by the variable vModelTriggerBeforeOnce.
385395
it('should not block input when another input listener with modifier is used', done => {
396+
let vModelTriggerBeforeOnce = false
397+
const captureInputValue = 'b'
398+
const onceInputValue = 'x'
386399
const vm = new Vue({
387400
data: {
388401
a: 'a',
389-
foo: false
402+
foo: false,
403+
b: 'b',
404+
bar: false,
390405
},
391406
template: `
392-
<div>
393-
<input ref="input" v-model="a" @input.capture="onInput">{{ a }}
394-
<div v-if="foo">foo</div>
395-
</div>
396-
`,
407+
<div>
408+
<input ref="input" v-model="a" @input.capture="onInput">{{ a }}
409+
<div v-if="foo">foo</div>
410+
<input ref="onceInput" v-model="b" @input.once="onInputBar">{{ b }}
411+
<div v-if="bar">bar</div>
412+
</div>
413+
`,
397414
methods: {
398415
onInput (e) {
399416
this.foo = true
417+
},
418+
onInputBar (e) {
419+
vModelTriggerBeforeOnce = this.b === onceInputValue
420+
this.bar = true
400421
}
401422
}
402423
}).$mount()
403424

404425
document.body.appendChild(vm.$el)
405426
vm.$refs.input.focus()
406-
vm.$refs.input.value = 'b'
427+
vm.$refs.input.value = captureInputValue
407428
triggerEvent(vm.$refs.input, 'input')
408429

430+
vm.$refs.onceInput.focus()
431+
vm.$refs.onceInput.value = onceInputValue
432+
triggerEvent(vm.$refs.onceInput, 'input')
433+
409434
// not using wait for update here because there will be two update cycles
410435
// one caused by onInput in the first listener
411436
setTimeout(() => {
412-
expect(vm.a).toBe('b')
413-
expect(vm.$refs.input.value).toBe('b')
437+
expect(vm.a).toBe(captureInputValue)
438+
expect(vm.foo).toBe(true)
439+
expect(vm.$refs.input.value).toBe(captureInputValue)
440+
441+
expect(vm.b).toBe(onceInputValue)
442+
expect(vm.bar).toBe(true)
443+
expect(vm.$refs.onceInput.value).toBe(onceInputValue)
444+
expect(vModelTriggerBeforeOnce).toBe(true)
414445
done()
415446
}, 16)
416447
})

0 commit comments

Comments
 (0)