Skip to content

Commit ad45767

Browse files
yyx990803hefeng
authored and
hefeng
committed
feat: use event delegation when possible
This also fixes async edge case vuejs#6566 where events propagate too slow and incorrectly trigger handlers post-patch.
1 parent 517efba commit ad45767

File tree

8 files changed

+140
-28
lines changed

8 files changed

+140
-28
lines changed

src/core/util/env.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const isEdge = UA && UA.indexOf('edge/') > 0
1414
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
1515
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
1616
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
17+
export const isPhantomJS = UA && /phantomjs/.test(UA)
1718

1819
// Firefox has a "watch" function on Object.prototype...
1920
export const nativeWatch = ({}).watch

src/platforms/web/runtime/modules/events.js

+113-15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { isDef, isUndef } from 'shared/util'
44
import { updateListeners } from 'core/vdom/helpers/index'
5-
import { isIE, supportsPassive } from 'core/util/index'
5+
import { isIE, isPhantomJS, supportsPassive } from 'core/util/index'
66
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
77

88
// normalize v-model event tokens that can only be determined at runtime.
@@ -38,32 +38,130 @@ function createOnceHandler (event, handler, capture) {
3838
}
3939
}
4040

41+
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/
42+
const eventCounts = {}
43+
const attachedGlobalHandlers = {}
44+
45+
type TargetRef = { el: Element | Document }
46+
4147
function add (
42-
event: string,
48+
name: string,
4349
handler: Function,
4450
capture: boolean,
4551
passive: boolean
4652
) {
47-
target.addEventListener(
48-
event,
49-
handler,
50-
supportsPassive
51-
? { capture, passive }
52-
: capture
53-
)
53+
if (!capture && !passive && delegateRE.test(name)) {
54+
const count = eventCounts[name]
55+
let store = target.__events
56+
if (!count) {
57+
attachGlobalHandler(name)
58+
}
59+
if (!store) {
60+
store = target.__events = {}
61+
}
62+
if (!store[name]) {
63+
eventCounts[name]++
64+
}
65+
store[name] = handler
66+
} else {
67+
target.addEventListener(
68+
name,
69+
handler,
70+
supportsPassive
71+
? { capture, passive }
72+
: capture
73+
)
74+
}
75+
}
76+
77+
function attachGlobalHandler(name: string) {
78+
const handler = (attachedGlobalHandlers[name] = (e: any) => {
79+
const isClick = e.type === 'click' || e.type === 'dblclick'
80+
if (isClick && e.button !== 0) {
81+
e.stopPropagation()
82+
return false
83+
}
84+
const targetRef: TargetRef = { el: document }
85+
dispatchEvent(e, name, isClick, targetRef)
86+
})
87+
document.addEventListener(name, handler)
88+
eventCounts[name] = 0
89+
}
90+
91+
function stopPropagation() {
92+
this.cancelBubble = true
93+
if (!this.immediatePropagationStopped) {
94+
this.stopImmediatePropagation()
95+
}
96+
}
97+
98+
function dispatchEvent(
99+
e: Event,
100+
name: string,
101+
isClick: boolean,
102+
targetRef: TargetRef
103+
) {
104+
let el: any = e.target
105+
let userEvent
106+
if (isPhantomJS) {
107+
// in PhantomJS it throws if we try to re-define currentTarget,
108+
// so instead we create a wrapped event to the user
109+
userEvent = Object.create((e: any))
110+
userEvent.stopPropagation = stopPropagation.bind((e: any))
111+
userEvent.preventDefault = e.preventDefault.bind(e)
112+
} else {
113+
userEvent = e
114+
}
115+
Object.defineProperty(userEvent, 'currentTarget', ({
116+
configurable: true,
117+
get() {
118+
return targetRef.el
119+
}
120+
}: any))
121+
while (el != null) {
122+
// Don't process clicks on disabled elements
123+
if (isClick && el.disabled) {
124+
break
125+
}
126+
const store = el.__events
127+
if (store) {
128+
const handler = store[name]
129+
if (handler) {
130+
targetRef.el = el
131+
handler(userEvent)
132+
if (e.cancelBubble) {
133+
break
134+
}
135+
}
136+
}
137+
el = el.parentNode
138+
}
139+
}
140+
141+
function removeGlobalHandler(name: string) {
142+
document.removeEventListener(name, attachedGlobalHandlers[name])
143+
attachedGlobalHandlers[name] = null
54144
}
55145

56146
function remove (
57-
event: string,
147+
name: string,
58148
handler: Function,
59149
capture: boolean,
60150
_target?: HTMLElement
61151
) {
62-
(_target || target).removeEventListener(
63-
event,
64-
handler._withTask || handler,
65-
capture
66-
)
152+
const el: any = _target || target
153+
if (!capture && delegateRE.test(name)) {
154+
el.__events[name] = null
155+
if (--eventCounts[name] === 0) {
156+
removeGlobalHandler(name)
157+
}
158+
} else {
159+
el.removeEventListener(
160+
name,
161+
handler._withTask || handler,
162+
capture
163+
)
164+
}
67165
}
68166

69167
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {

test/e2e/specs/async-edge-cases.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ module.exports = {
1515
.assert.checked('#case-1 input', false)
1616

1717
// // #6566
18-
// .assert.containsText('#case-2 button', 'Expand is True')
19-
// .assert.containsText('.count-a', 'countA: 0')
20-
// .assert.containsText('.count-b', 'countB: 0')
18+
.assert.containsText('#case-2 button', 'Expand is True')
19+
.assert.containsText('.count-a', 'countA: 0')
20+
.assert.containsText('.count-b', 'countB: 0')
2121

22-
// .click('#case-2 button')
23-
// .assert.containsText('#case-2 button', 'Expand is False')
24-
// .assert.containsText('.count-a', 'countA: 1')
25-
// .assert.containsText('.count-b', 'countB: 0')
22+
.click('#case-2 button')
23+
.assert.containsText('#case-2 button', 'Expand is False')
24+
.assert.containsText('.count-a', 'countA: 1')
25+
.assert.containsText('.count-b', 'countB: 0')
2626

27-
// .click('#case-2 button')
28-
// .assert.containsText('#case-2 button', 'Expand is True')
29-
// .assert.containsText('.count-a', 'countA: 1')
30-
// .assert.containsText('.count-b', 'countB: 1')
27+
.click('#case-2 button')
28+
.assert.containsText('#case-2 button', 'Expand is True')
29+
.assert.containsText('.count-a', 'countA: 1')
30+
.assert.containsText('.count-b', 'countB: 1')
3131

3232
.end()
3333
}

test/helpers/trigger-event.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
window.triggerEvent = function triggerEvent (target, event, process) {
22
const e = document.createEvent('HTMLEvents')
33
e.initEvent(event, true, true)
4+
if (event === 'click') {
5+
e.button = 0
6+
}
47
if (process) process(e)
58
target.dispatchEvent(e)
69
}

test/unit/features/component/component-slot.spec.js

+3
Original file line numberDiff line numberDiff line change
@@ -542,13 +542,16 @@ describe('Component slot', () => {
542542
}
543543
}).$mount()
544544

545+
document.body.appendChild(vm.$el)
545546
expect(vm.$el.textContent).toBe('hi')
546547
vm.$children[0].toggle = false
547548
waitForUpdate(() => {
548549
vm.$children[0].toggle = true
549550
}).then(() => {
550551
triggerEvent(vm.$el.querySelector('.click'), 'click')
551552
expect(spy).toHaveBeenCalled()
553+
}).then(() => {
554+
document.body.removeChild(vm.$el)
552555
}).then(done)
553556
})
554557

test/unit/features/directives/bind.spec.js

+4
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,12 @@ describe('Directive v-bind', () => {
157157
}
158158
}).$mount()
159159

160+
document.body.appendChild(vm.$el)
160161
expect(vm.$el.textContent).toBe('1')
161162
triggerEvent(vm.$el, 'click')
162163
waitForUpdate(() => {
163164
expect(vm.$el.textContent).toBe('2')
165+
document.body.removeChild(vm.$el)
164166
}).then(done)
165167
})
166168

@@ -227,13 +229,15 @@ describe('Directive v-bind', () => {
227229
}
228230
}
229231
}).$mount()
232+
document.body.appendChild(vm.$el)
230233
expect(vm.$el.textContent).toBe('1')
231234
triggerEvent(vm.$el, 'click')
232235
waitForUpdate(() => {
233236
expect(vm.$el.textContent).toBe('2')
234237
vm.test.fooBar = 3
235238
}).then(() => {
236239
expect(vm.$el.textContent).toBe('3')
240+
document.body.removeChild(vm.$el)
237241
}).then(done)
238242
})
239243

test/unit/features/directives/on.spec.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -735,10 +735,11 @@ describe('Directive v-on', () => {
735735

736736
it('should transform click.middle to mouseup', () => {
737737
const spy = jasmine.createSpy('click.middle')
738-
const vm = new Vue({
738+
vm = new Vue({
739+
el,
739740
template: `<div @click.middle="foo"></div>`,
740741
methods: { foo: spy }
741-
}).$mount()
742+
})
742743
triggerEvent(vm.$el, 'mouseup', e => { e.button = 0 })
743744
expect(spy).not.toHaveBeenCalled()
744745
triggerEvent(vm.$el, 'mouseup', e => { e.button = 1 })

test/unit/features/options/functional.spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ describe('Options functional', () => {
7070
}
7171
}).$mount()
7272

73+
document.body.appendChild(vm.$el)
7374
triggerEvent(vm.$el.children[0], 'click')
7475
expect(foo).toHaveBeenCalled()
7576
expect(foo.calls.argsFor(0)[0].type).toBe('click') // should have click event
7677
triggerEvent(vm.$el.children[0], 'mousedown')
7778
expect(bar).toHaveBeenCalledWith('bar')
79+
document.body.removeChild(vm.$el)
7880
})
7981

8082
it('should support returning more than one root node', () => {

0 commit comments

Comments
 (0)