From 587e7bdd1956d9fe6e7d1c21d3056ba431d45e0d Mon Sep 17 00:00:00 2001 From: cygeng Date: Mon, 21 Nov 2016 21:07:14 +0800 Subject: [PATCH 1/5] Modifier once for v-on --- src/compiler/helpers.js | 4 ++++ src/core/vdom/helpers/update-listeners.js | 9 +++++---- src/platforms/web/runtime/modules/events.js | 16 +++++++++++++++- test/unit/features/directives/on.spec.js | 18 ++++++++++++++++++ test/unit/modules/compiler/codegen.spec.js | 7 +++++++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/compiler/helpers.js b/src/compiler/helpers.js index 9a91e8bb6f8..f2bc8cadfb3 100644 --- a/src/compiler/helpers.js +++ b/src/compiler/helpers.js @@ -44,6 +44,10 @@ export function addHandler ( delete modifiers.capture name = '!' + name // mark the event as captured } + if (modifiers && modifiers.once) { + delete modifiers.once + name = '~' + name // mark the event as once + } let events if (modifiers && modifiers.native) { delete modifiers.native diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index 3feef8cbe39..6a231a4d5cf 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -9,7 +9,7 @@ export function updateListeners ( remove: Function, vm: Component ) { - let name, cur, old, fn, event, capture + let name, cur, old, fn, event, capture, once for (name in on) { cur = on[name] old = oldOn[name] @@ -20,9 +20,10 @@ export function updateListeners ( ) } else if (!old) { capture = name.charAt(0) === '!' - event = capture ? name.slice(1) : name + once = name.charAt(0) === '~' + event = capture || once ? name.slice(1) : name if (Array.isArray(cur)) { - add(event, (cur.invoker = arrInvoker(cur)), capture) + add(event, (cur.invoker = arrInvoker(cur)), capture, once) } else { if (!cur.invoker) { fn = cur @@ -30,7 +31,7 @@ export function updateListeners ( cur.fn = fn cur.invoker = fnInvoker(cur) } - add(event, cur.invoker, capture) + add(event, cur.invoker, capture, once) } } else if (cur !== old) { if (Array.isArray(old)) { diff --git a/src/platforms/web/runtime/modules/events.js b/src/platforms/web/runtime/modules/events.js index cf0c5441d99..0501b02a533 100644 --- a/src/platforms/web/runtime/modules/events.js +++ b/src/platforms/web/runtime/modules/events.js @@ -3,13 +3,27 @@ import { updateListeners } from 'core/vdom/helpers/index' +const onceFlags = {} +const randomHex = () => (Math.random() + '').substr(2).toString(16) + function updateDOMListeners (oldVnode, vnode) { if (!oldVnode.data.on && !vnode.data.on) { return } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} - const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture) => { + const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture, once) => { + if (once) { + const randomKey = randomHex() + new Date().valueOf().toString(16) + randomHex() + onceFlags[randomKey] = false + const oldHandler = handler + handler = () => { + if (!onceFlags[randomKey]) { + onceFlags[randomKey] = true + oldHandler() + } + } + } vnode.elm.addEventListener(event, handler, capture) }) const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler) => { diff --git a/test/unit/features/directives/on.spec.js b/test/unit/features/directives/on.spec.js index 301c8ec0412..691abfc1709 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -102,6 +102,24 @@ describe('Directive v-on', () => { expect(callOrder.toString()).toBe('1,2') }) + it('should support once', () => { + const numberPushed = [] + vm = new Vue({ + el, + template: ` +
+
+ `, + methods: { + foo () { numberPushed.push(1) } + } + }) + triggerEvent(vm.$el, 'click') + expect(numberPushed.toString()).toBe('1') + triggerEvent(vm.$el, 'click') + expect(numberPushed.toString()).toBe('1') + }) + it('should support keyCode', () => { vm = new Vue({ el, diff --git a/test/unit/modules/compiler/codegen.spec.js b/test/unit/modules/compiler/codegen.spec.js index f4759119237..30368acf5e0 100644 --- a/test/unit/modules/compiler/codegen.spec.js +++ b/test/unit/modules/compiler/codegen.spec.js @@ -256,6 +256,13 @@ describe('codegen', () => { ) }) + it('generate events with once modifier', () => { + assertCodegen( + '', + `with(this){return _h('input',{on:{"~input":function($event){onInput($event)}}})}` + ) + }) + it('generate events with inline statement', () => { assertCodegen( '', From 2da2a9df3247b58bda592dfc46ec571f5f95423c Mon Sep 17 00:00:00 2001 From: Changyu Geng Date: Mon, 21 Nov 2016 08:21:51 -0600 Subject: [PATCH 2/5] Reformat code --- src/compiler/helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/helpers.js b/src/compiler/helpers.js index f2bc8cadfb3..0ae213cf879 100644 --- a/src/compiler/helpers.js +++ b/src/compiler/helpers.js @@ -45,8 +45,8 @@ export function addHandler ( name = '!' + name // mark the event as captured } if (modifiers && modifiers.once) { - delete modifiers.once - name = '~' + name // mark the event as once + delete modifiers.once + name = '~' + name // mark the event as once } let events if (modifiers && modifiers.native) { From a900c52e64d5565f3a3d44215814898d1c745931 Mon Sep 17 00:00:00 2001 From: cygeng Date: Tue, 22 Nov 2016 18:17:27 +0800 Subject: [PATCH 3/5] Modifier once for v-on: using removeEventListener instead, bug fix of handler arguments passing, bug fix of modifier ordering problem --- src/core/vdom/helpers/update-listeners.js | 7 ++--- src/platforms/web/runtime/modules/events.js | 18 +++++-------- test/unit/features/directives/on.spec.js | 29 ++++++++++++++++----- test/unit/modules/compiler/codegen.spec.js | 14 ++++++++++ 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index 6a231a4d5cf..3ef9390b656 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -19,9 +19,10 @@ export function updateListeners ( vm ) } else if (!old) { - capture = name.charAt(0) === '!' - once = name.charAt(0) === '~' - event = capture || once ? name.slice(1) : name + once = name.charAt(0) === '~' // Prefixed last, checked first + event = once ? name.slice(1) : name + capture = event.charAt(0) === '!' + event = capture ? event.slice(1) : event if (Array.isArray(cur)) { add(event, (cur.invoker = arrInvoker(cur)), capture, once) } else { diff --git a/src/platforms/web/runtime/modules/events.js b/src/platforms/web/runtime/modules/events.js index 0501b02a533..063d6c5f2f4 100644 --- a/src/platforms/web/runtime/modules/events.js +++ b/src/platforms/web/runtime/modules/events.js @@ -3,9 +3,6 @@ import { updateListeners } from 'core/vdom/helpers/index' -const onceFlags = {} -const randomHex = () => (Math.random() + '').substr(2).toString(16) - function updateDOMListeners (oldVnode, vnode) { if (!oldVnode.data.on && !vnode.data.on) { return @@ -14,20 +11,17 @@ function updateDOMListeners (oldVnode, vnode) { const oldOn = oldVnode.data.on || {} const add = vnode.elm._v_add || (vnode.elm._v_add = (event, handler, capture, once) => { if (once) { - const randomKey = randomHex() + new Date().valueOf().toString(16) + randomHex() - onceFlags[randomKey] = false const oldHandler = handler - handler = () => { - if (!onceFlags[randomKey]) { - onceFlags[randomKey] = true - oldHandler() - } + handler = function (ev) { + remove(event, handler, capture) + + arguments.length === 1 ? oldHandler(ev) : oldHandler.apply(null, arguments) } } vnode.elm.addEventListener(event, handler, capture) }) - const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler) => { - vnode.elm.removeEventListener(event, handler) + const remove = vnode.elm._v_remove || (vnode.elm._v_remove = (event, handler, capture) => { + vnode.elm.removeEventListener(event, handler, capture) }) updateListeners(on, oldOn, add, remove, vnode.context) } diff --git a/test/unit/features/directives/on.spec.js b/test/unit/features/directives/on.spec.js index 691abfc1709..1649af4c0b5 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -103,21 +103,38 @@ describe('Directive v-on', () => { }) it('should support once', () => { - const numberPushed = [] vm = new Vue({ el, template: `
`, - methods: { - foo () { numberPushed.push(1) } - } + methods: { foo: spy } }) triggerEvent(vm.$el, 'click') - expect(numberPushed.toString()).toBe('1') + expect(spy.calls.count()).toBe(1) triggerEvent(vm.$el, 'click') - expect(numberPushed.toString()).toBe('1') + expect(spy.calls.count()).toBe(1) // should no longer trigger + }) + + it('should support capture and once', () => { + const callOrder = [] + vm = new Vue({ + el, + template: ` +
+
+
+ `, + methods: { + foo () { callOrder.push(1) }, + bar () { callOrder.push(2) } + } + }) + triggerEvent(vm.$el.firstChild, 'click') + expect(callOrder.toString()).toBe('1,2') + triggerEvent(vm.$el.firstChild, 'click') + expect(callOrder.toString()).toBe('1,2,2') }) it('should support keyCode', () => { diff --git a/test/unit/modules/compiler/codegen.spec.js b/test/unit/modules/compiler/codegen.spec.js index 30368acf5e0..ee971cadff6 100644 --- a/test/unit/modules/compiler/codegen.spec.js +++ b/test/unit/modules/compiler/codegen.spec.js @@ -263,6 +263,20 @@ describe('codegen', () => { ) }) + it('generate events with capture and once modifier', () => { + assertCodegen( + '', + `with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}` + ) + }) + + it('generate events with once and capture modifier', () => { + assertCodegen( + '', + `with(this){return _h('input',{on:{"~!input":function($event){onInput($event)}}})}` + ) + }) + it('generate events with inline statement', () => { assertCodegen( '', From 53c14db1514e31d3f9e1ff1e9b23cf8893036bb4 Mon Sep 17 00:00:00 2001 From: cygeng Date: Tue, 22 Nov 2016 18:19:10 +0800 Subject: [PATCH 4/5] Enhancement of event listener removal which allows rendering of capturing / once events for render function --- src/core/vdom/helpers/update-listeners.js | 7 +- test/unit/features/directives/on.spec.js | 82 +++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index 3ef9390b656..3523666bcf5 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -47,8 +47,11 @@ export function updateListeners ( } for (name in oldOn) { if (!on[name]) { - event = name.charAt(0) === '!' ? name.slice(1) : name - remove(event, oldOn[name].invoker) + once = name.charAt(0) === '~' // Prefixed last, checked first + event = once ? name.slice(1) : name + capture = event.charAt(0) === '!' + event = capture ? event.slice(1) : event + remove(event, oldOn[name].invoker, capture) // Removal of a capturing listener does not affect a non-capturing version of the same listener, and vice versa. } } } diff --git a/test/unit/features/directives/on.spec.js b/test/unit/features/directives/on.spec.js index 1649af4c0b5..b1f7da875c1 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -228,6 +228,88 @@ describe('Directive v-on', () => { }).then(done) }) + it('remove capturing listener', done => { + const spy2 = jasmine.createSpy('remove listener') + vm = new Vue({ + el, + methods: { foo: spy, bar: spy2 , stopped (ev) {ev.stopPropagation()} }, + data: { + ok: true + }, + render (h) { + return this.ok + ? h('div', { on: { '!click': this.foo }}, [h('div', { on: { click: this.stopped } })]) + : h('div', { on: { mouseOver: this.bar }}, [h('div')]) + } + }) + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) + expect(spy2.calls.count()).toBe(0) + vm.ok = false + waitForUpdate(() => { + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + triggerEvent(vm.$el, 'mouseOver') + expect(spy2.calls.count()).toBe(1) + }).then(done) + }) + + it('remove once listener', done => { + const spy2 = jasmine.createSpy('remove listener') + vm = new Vue({ + el, + methods: { foo: spy, bar: spy2 }, + data: { + ok: true + }, + render (h) { + return this.ok + ? h('input', { on: { '~click': this.foo }}) + : h('input', { on: { input: this.bar }}) + } + }) + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + expect(spy2.calls.count()).toBe(0) + vm.ok = false + waitForUpdate(() => { + triggerEvent(vm.$el, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + triggerEvent(vm.$el, 'input') + expect(spy2.calls.count()).toBe(1) + }).then(done) + }) + + it('remove capturing and once listener', done => { + const spy2 = jasmine.createSpy('remove listener') + vm = new Vue({ + el, + methods: { foo: spy, bar: spy2, stopped (ev) {ev.stopPropagation()} }, + data: { + ok: true + }, + render (h) { + return this.ok + ? h('div', { on: { '~!click': this.foo }}, [h('div', { on: { click: this.stopped } })]) + : h('div', { on: { mouseOver: this.bar }}, [h('div')]) + } + }) + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + expect(spy2.calls.count()).toBe(0) + vm.ok = false + waitForUpdate(() => { + triggerEvent(vm.$el.firstChild, 'click') + expect(spy.calls.count()).toBe(1) // should no longer trigger + triggerEvent(vm.$el, 'mouseOver') + expect(spy2.calls.count()).toBe(1) + }).then(done) + }) + it('remove listener on child component', done => { const spy2 = jasmine.createSpy('remove listener') vm = new Vue({ From 4f787196be961cd32f56181baccd2f15094b990a Mon Sep 17 00:00:00 2001 From: cygeng Date: Tue, 22 Nov 2016 18:29:28 +0800 Subject: [PATCH 5/5] Reformat code --- test/unit/features/directives/on.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/features/directives/on.spec.js b/test/unit/features/directives/on.spec.js index b1f7da875c1..70b76a90132 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -232,13 +232,13 @@ describe('Directive v-on', () => { const spy2 = jasmine.createSpy('remove listener') vm = new Vue({ el, - methods: { foo: spy, bar: spy2 , stopped (ev) {ev.stopPropagation()} }, + methods: { foo: spy, bar: spy2, stopped (ev) { ev.stopPropagation() } }, data: { ok: true }, render (h) { return this.ok - ? h('div', { on: { '!click': this.foo }}, [h('div', { on: { click: this.stopped } })]) + ? h('div', { on: { '!click': this.foo }}, [h('div', { on: { click: this.stopped }})]) : h('div', { on: { mouseOver: this.bar }}, [h('div')]) } }) @@ -286,13 +286,13 @@ describe('Directive v-on', () => { const spy2 = jasmine.createSpy('remove listener') vm = new Vue({ el, - methods: { foo: spy, bar: spy2, stopped (ev) {ev.stopPropagation()} }, + methods: { foo: spy, bar: spy2, stopped (ev) { ev.stopPropagation() } }, data: { ok: true }, render (h) { return this.ok - ? h('div', { on: { '~!click': this.foo }}, [h('div', { on: { click: this.stopped } })]) + ? h('div', { on: { '~!click': this.foo }}, [h('div', { on: { click: this.stopped }})]) : h('div', { on: { mouseOver: this.bar }}, [h('div')]) } })