mirror of
https://gitee.com/vuejs/vue.git
synced 2024-12-05 05:27:59 +08:00
feat: use event delegation when possible
This also fixes async edge case #6566 where events propagate too slow and incorrectly trigger handlers post-patch.
This commit is contained in:
parent
080dd971f7
commit
b7f7f27569
@ -14,6 +14,7 @@ export const isEdge = UA && UA.indexOf('edge/') > 0
|
|||||||
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
|
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
|
||||||
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
|
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
|
||||||
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
|
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
|
||||||
|
export const isPhantomJS = UA && /phantomjs/.test(UA)
|
||||||
|
|
||||||
// Firefox has a "watch" function on Object.prototype...
|
// Firefox has a "watch" function on Object.prototype...
|
||||||
export const nativeWatch = ({}).watch
|
export const nativeWatch = ({}).watch
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { isDef, isUndef } from 'shared/util'
|
import { isDef, isUndef } from 'shared/util'
|
||||||
import { updateListeners } from 'core/vdom/helpers/index'
|
import { updateListeners } from 'core/vdom/helpers/index'
|
||||||
import { isIE, supportsPassive } from 'core/util/index'
|
import { isIE, isPhantomJS, supportsPassive } from 'core/util/index'
|
||||||
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
|
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
|
||||||
|
|
||||||
// normalize v-model event tokens that can only be determined at runtime.
|
// normalize v-model event tokens that can only be determined at runtime.
|
||||||
@ -38,32 +38,130 @@ function createOnceHandler (event, handler, capture) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/
|
||||||
|
const eventCounts = {}
|
||||||
|
const attachedGlobalHandlers = {}
|
||||||
|
|
||||||
|
type TargetRef = { el: Element | Document }
|
||||||
|
|
||||||
function add (
|
function add (
|
||||||
event: string,
|
name: string,
|
||||||
handler: Function,
|
handler: Function,
|
||||||
capture: boolean,
|
capture: boolean,
|
||||||
passive: boolean
|
passive: boolean
|
||||||
) {
|
) {
|
||||||
target.addEventListener(
|
if (!capture && !passive && delegateRE.test(name)) {
|
||||||
event,
|
const count = eventCounts[name]
|
||||||
handler,
|
let store = target.__events
|
||||||
supportsPassive
|
if (!count) {
|
||||||
? { capture, passive }
|
attachGlobalHandler(name)
|
||||||
: capture
|
}
|
||||||
)
|
if (!store) {
|
||||||
|
store = target.__events = {}
|
||||||
|
}
|
||||||
|
if (!store[name]) {
|
||||||
|
eventCounts[name]++
|
||||||
|
}
|
||||||
|
store[name] = handler
|
||||||
|
} else {
|
||||||
|
target.addEventListener(
|
||||||
|
name,
|
||||||
|
handler,
|
||||||
|
supportsPassive
|
||||||
|
? { capture, passive }
|
||||||
|
: capture
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachGlobalHandler(name: string) {
|
||||||
|
const handler = (attachedGlobalHandlers[name] = (e: any) => {
|
||||||
|
const isClick = e.type === 'click' || e.type === 'dblclick'
|
||||||
|
if (isClick && e.button !== 0) {
|
||||||
|
e.stopPropagation()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const targetRef: TargetRef = { el: document }
|
||||||
|
dispatchEvent(e, name, isClick, targetRef)
|
||||||
|
})
|
||||||
|
document.addEventListener(name, handler)
|
||||||
|
eventCounts[name] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPropagation() {
|
||||||
|
this.cancelBubble = true
|
||||||
|
if (!this.immediatePropagationStopped) {
|
||||||
|
this.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchEvent(
|
||||||
|
e: Event,
|
||||||
|
name: string,
|
||||||
|
isClick: boolean,
|
||||||
|
targetRef: TargetRef
|
||||||
|
) {
|
||||||
|
let el: any = e.target
|
||||||
|
let userEvent
|
||||||
|
if (isPhantomJS) {
|
||||||
|
// in PhantomJS it throws if we try to re-define currentTarget,
|
||||||
|
// so instead we create a wrapped event to the user
|
||||||
|
userEvent = Object.create((e: any))
|
||||||
|
userEvent.stopPropagation = stopPropagation.bind((e: any))
|
||||||
|
userEvent.preventDefault = e.preventDefault.bind(e)
|
||||||
|
} else {
|
||||||
|
userEvent = e
|
||||||
|
}
|
||||||
|
Object.defineProperty(userEvent, 'currentTarget', ({
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return targetRef.el
|
||||||
|
}
|
||||||
|
}: any))
|
||||||
|
while (el != null) {
|
||||||
|
// Don't process clicks on disabled elements
|
||||||
|
if (isClick && el.disabled) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const store = el.__events
|
||||||
|
if (store) {
|
||||||
|
const handler = store[name]
|
||||||
|
if (handler) {
|
||||||
|
targetRef.el = el
|
||||||
|
handler(userEvent)
|
||||||
|
if (e.cancelBubble) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el = el.parentNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGlobalHandler(name: string) {
|
||||||
|
document.removeEventListener(name, attachedGlobalHandlers[name])
|
||||||
|
attachedGlobalHandlers[name] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove (
|
function remove (
|
||||||
event: string,
|
name: string,
|
||||||
handler: Function,
|
handler: Function,
|
||||||
capture: boolean,
|
capture: boolean,
|
||||||
_target?: HTMLElement
|
_target?: HTMLElement
|
||||||
) {
|
) {
|
||||||
(_target || target).removeEventListener(
|
const el: any = _target || target
|
||||||
event,
|
if (!capture && delegateRE.test(name)) {
|
||||||
handler._withTask || handler,
|
el.__events[name] = null
|
||||||
capture
|
if (--eventCounts[name] === 0) {
|
||||||
)
|
removeGlobalHandler(name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
el.removeEventListener(
|
||||||
|
name,
|
||||||
|
handler._withTask || handler,
|
||||||
|
capture
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
|
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
|
||||||
|
@ -15,19 +15,19 @@ module.exports = {
|
|||||||
.assert.checked('#case-1 input', false)
|
.assert.checked('#case-1 input', false)
|
||||||
|
|
||||||
// // #6566
|
// // #6566
|
||||||
// .assert.containsText('#case-2 button', 'Expand is True')
|
.assert.containsText('#case-2 button', 'Expand is True')
|
||||||
// .assert.containsText('.count-a', 'countA: 0')
|
.assert.containsText('.count-a', 'countA: 0')
|
||||||
// .assert.containsText('.count-b', 'countB: 0')
|
.assert.containsText('.count-b', 'countB: 0')
|
||||||
|
|
||||||
// .click('#case-2 button')
|
.click('#case-2 button')
|
||||||
// .assert.containsText('#case-2 button', 'Expand is False')
|
.assert.containsText('#case-2 button', 'Expand is False')
|
||||||
// .assert.containsText('.count-a', 'countA: 1')
|
.assert.containsText('.count-a', 'countA: 1')
|
||||||
// .assert.containsText('.count-b', 'countB: 0')
|
.assert.containsText('.count-b', 'countB: 0')
|
||||||
|
|
||||||
// .click('#case-2 button')
|
.click('#case-2 button')
|
||||||
// .assert.containsText('#case-2 button', 'Expand is True')
|
.assert.containsText('#case-2 button', 'Expand is True')
|
||||||
// .assert.containsText('.count-a', 'countA: 1')
|
.assert.containsText('.count-a', 'countA: 1')
|
||||||
// .assert.containsText('.count-b', 'countB: 1')
|
.assert.containsText('.count-b', 'countB: 1')
|
||||||
|
|
||||||
.end()
|
.end()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
window.triggerEvent = function triggerEvent (target, event, process) {
|
window.triggerEvent = function triggerEvent (target, event, process) {
|
||||||
const e = document.createEvent('HTMLEvents')
|
const e = document.createEvent('HTMLEvents')
|
||||||
e.initEvent(event, true, true)
|
e.initEvent(event, true, true)
|
||||||
|
if (event === 'click') {
|
||||||
|
e.button = 0
|
||||||
|
}
|
||||||
if (process) process(e)
|
if (process) process(e)
|
||||||
target.dispatchEvent(e)
|
target.dispatchEvent(e)
|
||||||
}
|
}
|
||||||
|
@ -542,6 +542,7 @@ describe('Component slot', () => {
|
|||||||
}
|
}
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
|
||||||
|
document.body.appendChild(vm.$el)
|
||||||
expect(vm.$el.textContent).toBe('hi')
|
expect(vm.$el.textContent).toBe('hi')
|
||||||
vm.$children[0].toggle = false
|
vm.$children[0].toggle = false
|
||||||
waitForUpdate(() => {
|
waitForUpdate(() => {
|
||||||
@ -549,6 +550,8 @@ describe('Component slot', () => {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
triggerEvent(vm.$el.querySelector('.click'), 'click')
|
triggerEvent(vm.$el.querySelector('.click'), 'click')
|
||||||
expect(spy).toHaveBeenCalled()
|
expect(spy).toHaveBeenCalled()
|
||||||
|
}).then(() => {
|
||||||
|
document.body.removeChild(vm.$el)
|
||||||
}).then(done)
|
}).then(done)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -157,10 +157,12 @@ describe('Directive v-bind', () => {
|
|||||||
}
|
}
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
|
||||||
|
document.body.appendChild(vm.$el)
|
||||||
expect(vm.$el.textContent).toBe('1')
|
expect(vm.$el.textContent).toBe('1')
|
||||||
triggerEvent(vm.$el, 'click')
|
triggerEvent(vm.$el, 'click')
|
||||||
waitForUpdate(() => {
|
waitForUpdate(() => {
|
||||||
expect(vm.$el.textContent).toBe('2')
|
expect(vm.$el.textContent).toBe('2')
|
||||||
|
document.body.removeChild(vm.$el)
|
||||||
}).then(done)
|
}).then(done)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -227,6 +229,7 @@ describe('Directive v-bind', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
document.body.appendChild(vm.$el)
|
||||||
expect(vm.$el.textContent).toBe('1')
|
expect(vm.$el.textContent).toBe('1')
|
||||||
triggerEvent(vm.$el, 'click')
|
triggerEvent(vm.$el, 'click')
|
||||||
waitForUpdate(() => {
|
waitForUpdate(() => {
|
||||||
@ -234,6 +237,7 @@ describe('Directive v-bind', () => {
|
|||||||
vm.test.fooBar = 3
|
vm.test.fooBar = 3
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
expect(vm.$el.textContent).toBe('3')
|
expect(vm.$el.textContent).toBe('3')
|
||||||
|
document.body.removeChild(vm.$el)
|
||||||
}).then(done)
|
}).then(done)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -735,10 +735,11 @@ describe('Directive v-on', () => {
|
|||||||
|
|
||||||
it('should transform click.middle to mouseup', () => {
|
it('should transform click.middle to mouseup', () => {
|
||||||
const spy = jasmine.createSpy('click.middle')
|
const spy = jasmine.createSpy('click.middle')
|
||||||
const vm = new Vue({
|
vm = new Vue({
|
||||||
|
el,
|
||||||
template: `<div @click.middle="foo"></div>`,
|
template: `<div @click.middle="foo"></div>`,
|
||||||
methods: { foo: spy }
|
methods: { foo: spy }
|
||||||
}).$mount()
|
})
|
||||||
triggerEvent(vm.$el, 'mouseup', e => { e.button = 0 })
|
triggerEvent(vm.$el, 'mouseup', e => { e.button = 0 })
|
||||||
expect(spy).not.toHaveBeenCalled()
|
expect(spy).not.toHaveBeenCalled()
|
||||||
triggerEvent(vm.$el, 'mouseup', e => { e.button = 1 })
|
triggerEvent(vm.$el, 'mouseup', e => { e.button = 1 })
|
||||||
|
@ -70,11 +70,13 @@ describe('Options functional', () => {
|
|||||||
}
|
}
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
|
||||||
|
document.body.appendChild(vm.$el)
|
||||||
triggerEvent(vm.$el.children[0], 'click')
|
triggerEvent(vm.$el.children[0], 'click')
|
||||||
expect(foo).toHaveBeenCalled()
|
expect(foo).toHaveBeenCalled()
|
||||||
expect(foo.calls.argsFor(0)[0].type).toBe('click') // should have click event
|
expect(foo.calls.argsFor(0)[0].type).toBe('click') // should have click event
|
||||||
triggerEvent(vm.$el.children[0], 'mousedown')
|
triggerEvent(vm.$el.children[0], 'mousedown')
|
||||||
expect(bar).toHaveBeenCalledWith('bar')
|
expect(bar).toHaveBeenCalledWith('bar')
|
||||||
|
document.body.removeChild(vm.$el)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support returning more than one root node', () => {
|
it('should support returning more than one root node', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user