# event
我们平时开发工作中,处理组件间的通讯,原生的交互,都离不开事件。对于一个组件元素,我们不仅仅可以绑定原生的 DOM 事件,还可以绑定自定义事件,非常灵活和方便。那么接下来我们从源码角度来看看它的实现原理。
为了更加直观,我们通过一个例子来分析它的实现:
let Child = {
template: '<button @click="clickHandler($event)">' +
'click me' +
'</button>',
methods: {
clickHandler(e) {
console.log('Button clicked!', e)
this.$emit('select')
}
}
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child @select="selectHandler" @click.native.prevent="clickHandler"></child>' +
'</div>',
methods: {
clickHandler() {
console.log('Child clicked!')
},
selectHandler() {
console.log('Child select!')
}
},
components: {
Child
}
})
# 编译
先从编译阶段开始看起,在 parse
阶段,会执行 processAttrs
方法,它的定义在 src/compiler/parser/index.js
中:
export const onRE = /^@|^v-on:/ // click事件
export const dirRE = /^v-|^@|^:/ // 自定义事件
export const bindRE = /^:|^v-bind:/ // 动态指令
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, isProp
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) { // 匹配v-或者@开头的指令
el.hasBindings = true
modifiers = parseModifiers(name)
if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind分支
// ..
} else if (onRE.test(name)) { // v-on分支
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn)
} else {
// ...
}
} else {
// ...
}
}
}
function parseModifiers (name: string): Object | void {
const match = name.match(modifierRE)
if (match) {
const ret = {}
match.forEach(m => { ret[m.slice(1)] = true })
return ret
}
}
在对标签属性的处理过程中,判断如果是指令,首先通过 parseModifiers
解析出修饰符,然后判断如果事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn)
方法,它的定义在 src/compiler/helpers.js
中:
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: Function
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.'
)
}
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = '!' + name // mark the event as captured
}
if (modifiers.once) {
delete modifiers.once
name = '~' + name // mark the event as once
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = '&' + name // mark the event as passive
}
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
if (name === 'click') {
if (modifiers.right) {
name = 'contextmenu'
delete modifiers.right
} else if (modifiers.middle) {
name = 'mouseup'
}
}
// events 用来记录绑定的事件
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = {
value: value.trim()
}
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
// 绑定的事件可以多个,回调也可以多个,最终会合并到数组中
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
addHandler
函数看起来长,实际上就做了 3 件事情,首先根据 modifier
修饰符对事件名 name
做处理,接着根据 modifier.native
判断是一个纯原生事件还是普通事件,分别对应 el.nativeEvents
和 el.events
,最后按照 name
对事件做归类,并把回调函数的字符串保留到对应的事件中。
在我们的例子中,父组件的 child
节点生成的 el.events
和 el.nativeEvents
如下:
el.events = {
select: {
value: 'selectHandler'
}
}
el.nativeEvents = {
click: {
value: 'clickHandler',
modifiers: {
prevent: true
}
}
}
// 子组件的button节点生成的el.event如下
el.events = {
click: {
value: 'clickHandler($event)'
}
}
然后在 codegen
的阶段,会在 genData
函数中根据 AST
元素节点上的 events
和 nativeEvents
生成 data
数据,它的定义在 src/compiler/codegen/index.js 中:
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// ...
if (el.events) {
data += `${genHandlers(el.events, false, state.warn)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true, state.warn)},`
}
// ...
return data
}