平时开发工作中,处理组件间的通讯,原生的交互,都离不开事件
对于一个组件元素,不仅仅可以绑定原生的 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:/export const dirRE = process.env.VBIND_PROP_SHORTHAND? /^v-|^@|^:|^\.|^#/: /^v-|^@|^:|^#/export const bindRE = /^:|^\.|^v-bind:/function processAttrs (el) {const list = el.attrsListlet i, l, name, rawName, value, modifiers, syncGen, isDynamicfor (i = 0, l = list.length; i < l; i++) {name = rawName = list[i].namevalue = list[i].valueif (dirRE.test(name)) { // 判断如果是指令// mark element as dynamicel.hasBindings = true// modifiersmodifiers = parseModifiers(name.replace(dirRE, '')) // 解析出修饰符// support .foo shorthand syntax for the .prop modifierif (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {(modifiers || (modifiers = {})).prop = truename = `.` + name.slice(1).replace(modifierRE, '')} else if (modifiers) {name = name.replace(modifierRE, '')}// 判断若是v-bind指令if (bindRE.test(name)) { // v-bindname = name.replace(bindRE, '')value = parseFilters(value)isDynamic = dynamicArgRE.test(name)if (isDynamic) {name = name.slice(1, -1)}if (process.env.NODE_ENV !== 'production' &&value.trim().length === 0) {warn(`The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`)}if (modifiers) {if (modifiers.prop && !isDynamic) {name = camelize(name)if (name === 'innerHtml') name = 'innerHTML'}if (modifiers.camel && !isDynamic) {name = camelize(name)}if (modifiers.sync) {syncGen = genAssignmentCode(value, `$event`)if (!isDynamic) {addHandler(el,`update:${camelize(name)}`,syncGen,null,false,warn,list[i])if (hyphenate(name) !== camelize(name)) {addHandler(el,`update:${hyphenate(name)}`,syncGen,null,false,warn,list[i])}} else {// handler w/ dynamic event nameaddHandler(el,`"update:"+(${name})`,syncGen,null,false,warn,list[i],true // dynamic)}}}if ((modifiers && modifiers.prop) || (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))) {addProp(el, name, value, list[i], isDynamic)} else {addAttr(el, name, value, list[i], isDynamic)}} else if (onRE.test(name)) { // v-onname = name.replace(onRE, '')isDynamic = dynamicArgRE.test(name)if (isDynamic) {name = name.slice(1, -1)}addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)} else { // normal directivesname = name.replace(dirRE, '')// parse argconst argMatch = name.match(argRE)let arg = argMatch && argMatch[1]isDynamic = falseif (arg) {name = name.slice(0, -(arg.length + 1))if (dynamicArgRE.test(arg)) {arg = arg.slice(1, -1)isDynamic = true}}addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])if (process.env.NODE_ENV !== 'production' && name === 'model') {checkForAliasModel(el, value)}}} else {// literal attributeif (process.env.NODE_ENV !== 'production') {const res = parseText(value, delimiters)if (res) {warn(`${name}="${value}": ` +'Interpolation inside attributes has been removed. ' +'Use v-bind or the colon shorthand instead. For example, ' +'instead of <div id="{{ val }}">, use <div :id="val">.',list[i])}}addAttr(el, name, JSON.stringify(value), list[i])// #6887 firefox doesn't update muted state if set via attribute// even immediately after element creationif (!el.component &&name === 'muted' &&platformMustUseProp(el.tag, el.attrsMap.type, name)) {addProp(el, name, 'true', list[i])}}}}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}}
addHandler定义在src/compiler/helpers.js中
export function addHandler (el: ASTElement,name: string,value: string,modifiers: ?ASTModifiers,important?: boolean,warn?: ?Function,range?: Range,dynamic?: boolean) {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.',range)}// 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 (modifiers.right) {if (dynamic) {name = `(${name})==='click'?'contextmenu':(${name})`} else if (name === 'click') {name = 'contextmenu'delete modifiers.right}} else if (modifiers.middle) { // 根据modifier修饰符对事件名name做处理if (dynamic) {name = `(${name})==='click'?'mouseup':(${name})`} else if (name === 'click') {name = 'mouseup'}}// check capture modifierif (modifiers.capture) {delete modifiers.capturename = prependModifierMarker('!', name, dynamic)}if (modifiers.once) {delete modifiers.oncename = prependModifierMarker('~', name, dynamic)}/* istanbul ignore if */if (modifiers.passive) {delete modifiers.passivename = prependModifierMarker('&', name, dynamic)}let eventsif (modifiers.native) { // 根据modifiers.native判断是一个纯原生事件el.nativeEvents还是普通事件el.eventsdelete modifiers.nativeevents = 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}// 按照name对事件做归类,并把回调函数的字符串保留到对应的事件中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}
在例子中父组件的child节点生成的el.events和el.nativeEvents如下
el.events = {select: {value: 'selectHandler'}}el.nativeEvents = {click: {value: 'clickHandler',modifiers: {prevent: true}}}
子组件的button节点生成的el.events如下
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 = '{'// ...// event handlersif (el.events) {data += `${genHandlers(el.events, false)},`}if (el.nativeEvents) {data += `${genHandlers(el.nativeEvents, true)},`}// ...return data}
genHandlers函数定义在src/compiler/codegen/event.js中
export function genHandlers (events: ASTElementHandlers,isNative: boolean): string {const prefix = isNative ? 'nativeOn:' : 'on:'let staticHandlers = ``let dynamicHandlers = ``// 遍历事件对象events 对同一个事件名称的事件调用genHandler(events[name])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}}function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {if (!handler) {return 'function(){}'}// handler是一个数组就遍历递归调用genHandler方法并拼接结果if (Array.isArray(handler)) {return `[${handler.map(handler => genHandler(handler)).join(',')}]`}// 判断handler.value是一个函数的调用路径还是一个函数表达式const isMethodPath = simplePathRE.test(handler.value)const isFunctionExpression = fnExpRE.test(handler.value)const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ''))if (!handler.modifiers) { // 对于没有modifiers的情况就根据handler.value不同情况处理,要么直接返回,要么返回一个函数包裹的表达式if (isMethodPath || isFunctionExpression) {return handler.value}/* istanbul ignore if */if (__WEEX__ && handler.params) {return genWeexHandler(handler.params, handler.value)}return `function($event){${isFunctionInvocation ? `return ${handler.value}` : handler.value}}` // inline statement} else { // 对于有modifiers的情况则对各种不同modifer情况做不同处理,添加相应的代码串let code = ''let genModifierCode = ''const keys = []for (const key in handler.modifiers) {if (modifierCode[key]) {genModifierCode += modifierCode[key]// left/rightif (keyCodes[key]) {keys.push(key)}} else if (key === 'exact') {const modifiers: ASTModifiers = (handler.modifiers: any)genModifierCode += genGuard(['ctrl', 'shift', 'alt', 'meta'].filter(keyModifier => !modifiers[keyModifier]).map(keyModifier => `$event.${keyModifier}Key`).join('||'))} else {keys.push(key)}}if (keys.length) {code += genKeyFilter(keys)}// Make sure modifiers like prevent and stop get executed after key filteringif (genModifierCode) {code += genModifierCode}const handlerCode = isMethodPath? `return ${handler.value}.apply(null, arguments)`: isFunctionExpression? `return (${handler.value}).apply(null, arguments)`: isFunctionInvocation? `return ${handler.value}`: handler.value/* istanbul ignore if */if (__WEEX__ && handler.params) {return genWeexHandler(handler.params, code + handlerCode)}return `function($event){${code}${handlerCode}}`}}
对于例子而言父组件生成的data串为
{on: {"select": selectHandler},nativeOn: {"click": function($event) {$event.preventDefault();return clickHandler($event)}}}
子组件生成的data串为
{on: {"click": function($event) {clickHandler($event)}}}
编译部分完了,接下来看运行时部分是如何实现的
Vue 的事件有 2 种,一种是原生 DOM 事件,一种是用户自定义事件
DOM事件
之前在 patch 的时候执行各种 module 的钩子函数时这部分是略过的,只分析了 DOM 是如何渲染的,而 DOM 元素相关的属性、样式、事件等都是通过这些 module 的钩子函数完成设置的
所有和 web 相关的 module 都定义在 src/platforms/web/runtime/modules 目录下,这次只关注目录下的 events.js 即可
在 patch 过程中的创建阶段和更新阶段都会执行 updateDOMListeners
let target: anyfunction updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {return}// vnode.data.on - data中对应的事件对象const on = vnode.data.on || {}const oldOn = oldVnode.data.on || {}// vnode is empty when removing all listeners,// and use old vnode dom element// 当前vnode对应的DOM对象target = vnode.elm || oldVnode.elm// 对v-model相关的处理normalizeEvents(on)updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)target = undefined}
updateListeners定义在src/core/vdom/helpers/update-listeners.js中
// add、remove方法都是外部传入的,因为它既处理原生DOM事件的添加删除,也处理自定义事件的添加删除export function updateListeners (on: Object,oldOn: Object,add: Function,remove: Function,createOnceHandler: Function,vm: Component) {let name, def, cur, old, event// 遍历on去添加事件监听for (name in on) {def = cur = on[name]old = oldOn[name]event = normalizeEvent(name) // 对事件名做处理/* istanbul ignore if */if (__WEEX__ && isPlainObject(def)) {cur = def.handlerevent.params = def.params}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(event.name, cur, event.capture, event.passive, event.params) // 完成一次事件绑定} else if (cur !== old) { // 当第二次执行该函数的时候,判断cur !== oldold.fns = cur // 更改 old.fns = cur 把之前绑定的 involer.fns 赋值为新的回调函数on[name] = old // 保留引用关系,这样就保证了事件回调只添加一次,之后仅仅去修改它的回调函数的引用}}// 遍历oldOn去移除事件监听for (name in oldOn) {if (isUndef(on[name])) {event = normalizeEvent(name)remove(event.name, oldOn[name], event.capture)}}}
normalizeEvent
const normalizeEvent = cached((name: string): {name: string,once: boolean,capture: boolean,passive: boolean,handler?: Function,params?: Array<any>} => {// 根据事件名的一些特殊标识(addHandler时添加的)区分事件是否有once、capture、passive修饰符const passive = name.charAt(0) === '&'name = passive ? name.slice(1) : nameconst once = name.charAt(0) === '~' // Prefixed last, checked firstname = once ? name.slice(1) : nameconst capture = name.charAt(0) === '!'name = capture ? name.slice(1) : namereturn {name,once,capture,passive}})
createFnInvoker
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {// 定义了invoker方法并返回function invoker () {const fns = invoker.fns// 由于一个事件可能会对应多个回调函数,所以做了数组的判断,多个回调函数就依次调用if (Array.isArray(fns)) {const cloned = fns.slice()for (let i = 0; i < cloned.length; i++) {invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)}} else {// return handler return value for single handlersreturn invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)}}// 每一次执行invoker函数都是从invoker.fns里取执行的回调函数invoker.fns = fnsreturn invoker}
在原生DOM事件中真正添加回调和移除回调函数的实现
定义在 src/platforms/web/runtime/modules/event.js 中
function add (name: string,handler: Function,capture: boolean,passive: boolean) {// async edge case #6566: inner click event triggers patch, event handler// attached to outer element during patch, and triggered again. This// happens because browsers fire microtask ticks between event propagation.// the solution is simple: we save the timestamp when a handler is attached,// and the handler would only fire if the event passed to it was fired// AFTER it was attached.if (useMicrotaskFix) {const attachedTimestamp = currentFlushTimestampconst original = handlerhandler = original._wrapper = function (e) {if (// no bubbling, should always fire.// this is just a safety net in case event.timeStamp is unreliable in// certain weird environments...e.target === e.currentTarget ||// event is fired after handler attachmente.timeStamp >= attachedTimestamp ||// bail for environments that have buggy event.timeStamp implementations// #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState// #9681 QtWebEngine event.timeStamp is negative valuee.timeStamp <= 0 ||// #9448 bail if event is fired in another document in a multi-page// electron/nw.js app, since event.timeStamp will be using a different// starting referencee.target.ownerDocument !== document) {return original.apply(this, arguments)}}}target.addEventListener(name,handler,supportsPassive? { capture, passive }: capture)}function remove (name: string,handler: Function,capture: boolean,_target?: HTMLElement) {(_target || target).removeEventListener(name,handler._wrapper || handler,capture)}
实际上调用原生 addEventListener 和 removeEventListener,并根据参数传递一些配置
自定义事件
自定义事件只能作用在组件上,如果在组件上使用原生事件,需要加 .native 修饰符,普通元素上使用 .native 修饰符无效
在 render 阶段,如果是一个组件节点,则通过 createComponent 创建一个组件 vnode
定义在src/core/vdom/create-component.js中
export function createComponent (Ctor: Class<Component> | Function | Object | void,data: ?VNodeData,context: Component,children: ?Array<VNode>,tag?: string): VNode | Array<VNode> | void {// ...// extract listeners, since these needs to be treated as// child component listeners instead of DOM listenersconst listeners = data.on// replace with listeners with .native modifier// so it gets processed during parent component patch.data.on = data.nativeOn// ...// return a placeholder vnodeconst name = Ctor.options.name || tag// 把listeners作为vnode的componentOptions传入,它是在子组件初始化阶段中处理的,所以它的处理环境是子组件const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,data, undefined, undefined, undefined, context,{ Ctor, propsData, listeners, tag, children },asyncFactory)// ...return vnode}
在子组件的初始化的时候,会执行initInternalComponent方法
定义在 src/core/instance/init.js 中
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {const opts = vm.$options = Object.create(vm.constructor.options)// doing this because it's faster than dynamic enumeration.const parentVnode = options._parentVnodeopts.parent = options.parentopts._parentVnode = parentVnodeconst vnodeComponentOptions = parentVnode.componentOptionsopts.propsData = vnodeComponentOptions.propsData// 拿到父组件传入的listenersopts._parentListeners = vnodeComponentOptions.listenersopts._renderChildren = vnodeComponentOptions.childrenopts._componentTag = vnodeComponentOptions.tagif (options.render) {opts.render = options.renderopts.staticRenderFns = options.staticRenderFns}}
在执行 initEvents 的过程中,会处理这个 listeners
定义在 src/core/instance/events.js 中
export function initEvents (vm: Component) {vm._events = Object.create(null)vm._hasHookEvent = false// init parent attached eventsconst listeners = vm.$options._parentListeners// 拿到listeners后if (listeners) {updateComponentListeners(vm, listeners)}}
updateComponentListeners
let target: anyexport function updateComponentListeners (vm: Component,listeners: Object,oldListeners: ?Object) {target = vm// 对于自定义事件和原生DOM事件处理的差异就在事件添加和删除的实现上updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)target = undefined}
自定义事件add和remove
// 实际上是利用Vue定义的事件中心function add (event, fn) {target.$on(event, fn)}function remove (event, fn) {target.$off(event, fn)}
实现
// 非常经典的事件中心的实现,把所有的事件用 vm._events 存储起来export function eventsMixin (Vue: Class<Component>) {const hookRE = /^hook:/Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {const vm: Component = thisif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$on(event[i], fn)}} else {(vm._events[event] || (vm._events[event] = [])).push(fn) // 按事件名称把回调函数fn存储起来// optimize hook:event cost by using a boolean flag marked at registration// instead of a hash lookupif (hookRE.test(event)) {vm._hasHookEvent = true}}return vm}// 内部执行vm.$on,并且当回调函数执行一次后再通过vm.$off移除事件的回调。这样来确保回调函数只执行一次Vue.prototype.$once = function (event: string, fn: Function): Component {const vm: Component = thisfunction on () {vm.$off(event, on)fn.apply(vm, arguments)}on.fn = fnvm.$on(event, on)return vm}// 移除指定事件名event和指定的fnVue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {const vm: Component = this// allif (!arguments.length) {vm._events = Object.create(null)return vm}// array of eventsif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$off(event[i], fn)}return vm}// specific eventconst cbs = vm._events[event]if (!cbs) {return vm}if (!fn) {vm._events[event] = nullreturn vm}// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}return vm}Vue.prototype.$emit = function (event: string): Component {const vm: Component = thisif (process.env.NODE_ENV !== 'production') {const lowerCaseEvent = event.toLowerCase()if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {tip(`Event "${lowerCaseEvent}" is emitted in component ` +`${formatComponentName(vm)} but the handler is registered for "${event}". ` +`Note that HTML attributes are case-insensitive and you cannot use ` +`v-on to listen to camelCase events when using in-DOM templates. ` +`You should probably use "${hyphenate(event)}" instead of "${event}".`)}}// 根据事件名event找到所有回调函数let cbs = vm._events[event]if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbsconst 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}}
对于用户自定义的事件添加和删除就是利用了这几个事件中心的 API
需要注意的一点,vm.$emit 是给当前的 vm 上派发的实例,之所以常用它做父子组件通讯,是因为它的回调函数的定义是在父组件中
对于例子而言,当子组件的 button 被点击了,它通过 this.$emit(‘select’) 派发事件,那么子组件的实例就监听到了这个 select 事件,并执行它的回调函数——定义在父组件中的 selectHandler 方法,这样就相当于完成了一次父子组件的通讯
Vue 支持 2 种事件类型,原生 DOM 事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。另外要注意一点,只有组件节点才可以添加自定义事件,并且添加原生 DOM 事件需要使用 native 修饰符;而普通元素使用 .native 修饰符是没有作用的,也只能添加原生 DOM 事件
