一、背景
v-on指令在vue中用于绑定DOM/自定义事件的响应处理函数,可以用在HMTL原生标签上,也可以用于自定义的vue组件标签上。
先看一下简单的使用示例:
// App.vue
<template>
<div id="app">
<button @click="handleButtonClick"></button>
<HelloWorld @customEvent="handleCustomEvent" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld'
export default {
name: 'App',
components: {
HelloWorld
},
methods: {
handleButtonClick(){
console.log('button clicked.')
},
handleCustomEvent(){
console.log('custom event received.')
}
},
}
</script>
// HelloWorld.vue
<template>
<div class="hello">
</div>
</template>
<script>
export default {
name: 'HelloWorld',
mounted() {
this.$emit('customEvent')
}
}
</script>
那么v-on具体是如何实现的呢,通过源码来分析一波。
二、父组件模板编译时的处理
从上面示例中看到,v-on指令是在模板(即template标签中)的,只要在模板中的事物,一定会经过模板编译的处理,模板编译大致上可以分为AST和Codegen。
2.1:AST处理
v-on指令,作为标签上的属性,在AST时经过processAttrs方法处理
// vue-2.6.11源码 src\compiler\parser\index.js
// 仅贴出部分相关代码
function processAttrs (el){
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
// line 844-850
if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
}
}
}
// vue-2.6.11源码 src\compiler\parser\helper.js
// 仅贴出部分相关代码
export function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// 省略很多处理事件修饰符的代码
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
/* istanbul ignore if */
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
}
AST处理后,会在对应的AST节点上生成events属性,events属性是一个对象,key值是v-on绑定的事件名称,值是事件的响应函数。
2.2:Codegen处理
AST处理后生成的节点树,会经过Codegen处理生成渲染函数
// vue-2.6.11源码 src\compiler\codegen\index.js
// 仅贴出部分相关代码
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
return data
}
// vue-2.6.11源码 src\compiler\codegen\event.js
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) {
const handlerCode = genHandler(events[name])
if (events[name] && events[name].dynamic) {
dynamicHandlers += `${name},${handlerCode},`
} else {
staticHandlers += `"${name}":${handlerCode},`
}
}
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
if (dynamicHandlers) {
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
} else {
return prefix + staticHandlers
}
}
总结:codegen时递归的对每一个AST节点进行处理。针对events属性,最终的data属性中有一个on属性(如果有native事件,还会有nativeOn属性),on属性的值也是一个对象,其中的key值是事件名称,value值是事件响应函数。
2.3:最终生成的render函数
我们看一下App.vue实际上生成的render函数:
// App.vue 编译后的render函数
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ attrs: { id: "app" } },
[
_c("button", { on: { click: _vm.handleButtonClick } }),
_vm._v(" "),
_c("HelloWorld", { on: { customEvent: _vm.handleCustomEvent } })
],
1
)
}
从以上渲染函数可以看出,button和HelloWorld节点生成了data数据,其中有on属性,和源码中分析的情况一致。
三、自定义组件中的事件处理
在实例化vue组件时,会进行初始化处理,在初始化时就会对events属性进行处理
// vue-2.6.11源码 src\core\instance\events.js
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// 渲染函数中的data参数(createELement的第二个参数),经过处理,会成为属性挂在$options上
//_parentListeners属性对应的就是on属性
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
function add (event, fn) {
// 调用原型上的$on方法注册事件和响应,所以直接使用vm.$on也可以进行事件绑定注册
target.$on(event, fn)
}
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 重点:最终事件和响应存储在vm._events属性中
(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
// vue-2.6.11源码 src\core\vdom\helpers\update-listeners.js
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
/* istanbul ignore if */
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
// 调用add方法
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
总结起来就是,事件和对应的响应最终存储在vm._events属性中。
我们知道事件在子组件中通过emit的代码:
// vue-2.6.11源码 src\core\instance\events.js
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
可以看到$emit的逻辑很简单,就是从vm._events属性中取对应事件的响应函数,然后执行。
四、原生标签上的事件处理
上一节我们查看了vue组件中对events的处理过程,但是这些仅限于vue组件;
对于原生标签使用了更为简单的处理,即使用原生DOM自带的addEventListener API:
//vue-2.6.11源码 src\platforms\web\runtime\modules\events.js
function add (
name: string,
handler: Function,
capture: boolean,
passive: boolean
) {
if (useMicrotaskFix) {
const attachedTimestamp = currentFlushTimestamp
const original = handler
handler = original._wrapper = function (e) {
if (
e.target === e.currentTarget ||
e.timeStamp >= attachedTimestamp ||
e.timeStamp <= 0 ||
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
}
}
// 使用addEventListener API进行事件绑定
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
总结:原生标签采用addEventListener进行事件注册。