平时开发工作中,处理组件间的通讯,原生的交互,都离不开事件
对于一个组件元素,不仅仅可以绑定原生的 DOM 事件,还可以绑定自定义事件,非常灵活和方便

例子

  1. let Child = {
  2. template: '<button @click="clickHandler($event)">' +
  3. 'click me' +
  4. '</button>',
  5. methods: {
  6. clickHandler(e) {
  7. console.log('Button clicked!', e)
  8. this.$emit('select')
  9. }
  10. }
  11. }
  12. let vm = new Vue({
  13. el: '#app',
  14. template: '<div>' +
  15. '<child @select="selectHandler" @click.native.prevent="clickHandler"></child>' +
  16. '</div>',
  17. methods: {
  18. clickHandler() {
  19. console.log('Child clicked!')
  20. },
  21. selectHandler() {
  22. console.log('Child select!')
  23. }
  24. },
  25. components: {
  26. Child
  27. }
  28. })

编译

先从编译阶段开始看起,在 parse 阶段,会执行 processAttrs 方法
定义在src/compiler/parser/index.js中

  1. export const onRE = /^@|^v-on:/
  2. export const dirRE = process.env.VBIND_PROP_SHORTHAND
  3. ? /^v-|^@|^:|^\.|^#/
  4. : /^v-|^@|^:|^#/
  5. export const bindRE = /^:|^\.|^v-bind:/
  6. function processAttrs (el) {
  7. const list = el.attrsList
  8. let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  9. for (i = 0, l = list.length; i < l; i++) {
  10. name = rawName = list[i].name
  11. value = list[i].value
  12. if (dirRE.test(name)) { // 判断如果是指令
  13. // mark element as dynamic
  14. el.hasBindings = true
  15. // modifiers
  16. modifiers = parseModifiers(name.replace(dirRE, '')) // 解析出修饰符
  17. // support .foo shorthand syntax for the .prop modifier
  18. if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
  19. (modifiers || (modifiers = {})).prop = true
  20. name = `.` + name.slice(1).replace(modifierRE, '')
  21. } else if (modifiers) {
  22. name = name.replace(modifierRE, '')
  23. }
  24. // 判断若是v-bind指令
  25. if (bindRE.test(name)) { // v-bind
  26. name = name.replace(bindRE, '')
  27. value = parseFilters(value)
  28. isDynamic = dynamicArgRE.test(name)
  29. if (isDynamic) {
  30. name = name.slice(1, -1)
  31. }
  32. if (
  33. process.env.NODE_ENV !== 'production' &&
  34. value.trim().length === 0
  35. ) {
  36. warn(
  37. `The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
  38. )
  39. }
  40. if (modifiers) {
  41. if (modifiers.prop && !isDynamic) {
  42. name = camelize(name)
  43. if (name === 'innerHtml') name = 'innerHTML'
  44. }
  45. if (modifiers.camel && !isDynamic) {
  46. name = camelize(name)
  47. }
  48. if (modifiers.sync) {
  49. syncGen = genAssignmentCode(value, `$event`)
  50. if (!isDynamic) {
  51. addHandler(
  52. el,
  53. `update:${camelize(name)}`,
  54. syncGen,
  55. null,
  56. false,
  57. warn,
  58. list[i]
  59. )
  60. if (hyphenate(name) !== camelize(name)) {
  61. addHandler(
  62. el,
  63. `update:${hyphenate(name)}`,
  64. syncGen,
  65. null,
  66. false,
  67. warn,
  68. list[i]
  69. )
  70. }
  71. } else {
  72. // handler w/ dynamic event name
  73. addHandler(
  74. el,
  75. `"update:"+(${name})`,
  76. syncGen,
  77. null,
  78. false,
  79. warn,
  80. list[i],
  81. true // dynamic
  82. )
  83. }
  84. }
  85. }
  86. if ((modifiers && modifiers.prop) || (
  87. !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
  88. )) {
  89. addProp(el, name, value, list[i], isDynamic)
  90. } else {
  91. addAttr(el, name, value, list[i], isDynamic)
  92. }
  93. } else if (onRE.test(name)) { // v-on
  94. name = name.replace(onRE, '')
  95. isDynamic = dynamicArgRE.test(name)
  96. if (isDynamic) {
  97. name = name.slice(1, -1)
  98. }
  99. addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
  100. } else { // normal directives
  101. name = name.replace(dirRE, '')
  102. // parse arg
  103. const argMatch = name.match(argRE)
  104. let arg = argMatch && argMatch[1]
  105. isDynamic = false
  106. if (arg) {
  107. name = name.slice(0, -(arg.length + 1))
  108. if (dynamicArgRE.test(arg)) {
  109. arg = arg.slice(1, -1)
  110. isDynamic = true
  111. }
  112. }
  113. addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
  114. if (process.env.NODE_ENV !== 'production' && name === 'model') {
  115. checkForAliasModel(el, value)
  116. }
  117. }
  118. } else {
  119. // literal attribute
  120. if (process.env.NODE_ENV !== 'production') {
  121. const res = parseText(value, delimiters)
  122. if (res) {
  123. warn(
  124. `${name}="${value}": ` +
  125. 'Interpolation inside attributes has been removed. ' +
  126. 'Use v-bind or the colon shorthand instead. For example, ' +
  127. 'instead of <div id="{{ val }}">, use <div :id="val">.',
  128. list[i]
  129. )
  130. }
  131. }
  132. addAttr(el, name, JSON.stringify(value), list[i])
  133. // #6887 firefox doesn't update muted state if set via attribute
  134. // even immediately after element creation
  135. if (!el.component &&
  136. name === 'muted' &&
  137. platformMustUseProp(el.tag, el.attrsMap.type, name)) {
  138. addProp(el, name, 'true', list[i])
  139. }
  140. }
  141. }
  142. }
  143. function parseModifiers (name: string): Object | void {
  144. const match = name.match(modifierRE)
  145. if (match) {
  146. const ret = {}
  147. match.forEach(m => { ret[m.slice(1)] = true })
  148. return ret
  149. }
  150. }

addHandler定义在src/compiler/helpers.js中

  1. export function addHandler (
  2. el: ASTElement,
  3. name: string,
  4. value: string,
  5. modifiers: ?ASTModifiers,
  6. important?: boolean,
  7. warn?: ?Function,
  8. range?: Range,
  9. dynamic?: boolean
  10. ) {
  11. modifiers = modifiers || emptyObject
  12. // warn prevent and passive modifier
  13. /* istanbul ignore if */
  14. if (
  15. process.env.NODE_ENV !== 'production' && warn &&
  16. modifiers.prevent && modifiers.passive
  17. ) {
  18. warn(
  19. 'passive and prevent can\'t be used together. ' +
  20. 'Passive handler can\'t prevent default event.',
  21. range
  22. )
  23. }
  24. // normalize click.right and click.middle since they don't actually fire
  25. // this is technically browser-specific, but at least for now browsers are
  26. // the only target envs that have right/middle clicks.
  27. if (modifiers.right) {
  28. if (dynamic) {
  29. name = `(${name})==='click'?'contextmenu':(${name})`
  30. } else if (name === 'click') {
  31. name = 'contextmenu'
  32. delete modifiers.right
  33. }
  34. } else if (modifiers.middle) { // 根据modifier修饰符对事件名name做处理
  35. if (dynamic) {
  36. name = `(${name})==='click'?'mouseup':(${name})`
  37. } else if (name === 'click') {
  38. name = 'mouseup'
  39. }
  40. }
  41. // check capture modifier
  42. if (modifiers.capture) {
  43. delete modifiers.capture
  44. name = prependModifierMarker('!', name, dynamic)
  45. }
  46. if (modifiers.once) {
  47. delete modifiers.once
  48. name = prependModifierMarker('~', name, dynamic)
  49. }
  50. /* istanbul ignore if */
  51. if (modifiers.passive) {
  52. delete modifiers.passive
  53. name = prependModifierMarker('&', name, dynamic)
  54. }
  55. let events
  56. if (modifiers.native) { // 根据modifiers.native判断是一个纯原生事件el.nativeEvents还是普通事件el.events
  57. delete modifiers.native
  58. events = el.nativeEvents || (el.nativeEvents = {})
  59. } else {
  60. events = el.events || (el.events = {})
  61. }
  62. const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
  63. if (modifiers !== emptyObject) {
  64. newHandler.modifiers = modifiers
  65. }
  66. // 按照name对事件做归类,并把回调函数的字符串保留到对应的事件中
  67. const handlers = events[name]
  68. /* istanbul ignore if */
  69. if (Array.isArray(handlers)) {
  70. important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  71. } else if (handlers) {
  72. events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
  73. } else {
  74. events[name] = newHandler
  75. }
  76. el.plain = false
  77. }

在例子中父组件的child节点生成的el.events和el.nativeEvents如下

  1. el.events = {
  2. select: {
  3. value: 'selectHandler'
  4. }
  5. }
  6. el.nativeEvents = {
  7. click: {
  8. value: 'clickHandler',
  9. modifiers: {
  10. prevent: true
  11. }
  12. }
  13. }

子组件的button节点生成的el.events如下

  1. el.events = {
  2. click: {
  3. value: 'clickHandler($event)'
  4. }
  5. }

在codegen的阶段会在genData函数中根据AST元素节点上的events和nativeEvents生成的data数据
定义在src/compiler/codegen/index.js中

  1. export function genData (el: ASTElement, state: CodegenState): string {
  2. let data = '{'
  3. // ...
  4. // event handlers
  5. if (el.events) {
  6. data += `${genHandlers(el.events, false)},`
  7. }
  8. if (el.nativeEvents) {
  9. data += `${genHandlers(el.nativeEvents, true)},`
  10. }
  11. // ...
  12. return data
  13. }

genHandlers函数定义在src/compiler/codegen/event.js中

  1. export function genHandlers (
  2. events: ASTElementHandlers,
  3. isNative: boolean
  4. ): string {
  5. const prefix = isNative ? 'nativeOn:' : 'on:'
  6. let staticHandlers = ``
  7. let dynamicHandlers = ``
  8. // 遍历事件对象events 对同一个事件名称的事件调用genHandler(events[name])
  9. for (const name in events) {
  10. const handlerCode = genHandler(events[name])
  11. if (events[name] && events[name].dynamic) {
  12. dynamicHandlers += `${name},${handlerCode},`
  13. } else {
  14. staticHandlers += `"${name}":${handlerCode},`
  15. }
  16. }
  17. staticHandlers = `{${staticHandlers.slice(0, -1)}}`
  18. if (dynamicHandlers) {
  19. return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
  20. } else {
  21. return prefix + staticHandlers
  22. }
  23. }
  24. function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
  25. if (!handler) {
  26. return 'function(){}'
  27. }
  28. // handler是一个数组就遍历递归调用genHandler方法并拼接结果
  29. if (Array.isArray(handler)) {
  30. return `[${handler.map(handler => genHandler(handler)).join(',')}]`
  31. }
  32. // 判断handler.value是一个函数的调用路径还是一个函数表达式
  33. const isMethodPath = simplePathRE.test(handler.value)
  34. const isFunctionExpression = fnExpRE.test(handler.value)
  35. const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ''))
  36. if (!handler.modifiers) { // 对于没有modifiers的情况就根据handler.value不同情况处理,要么直接返回,要么返回一个函数包裹的表达式
  37. if (isMethodPath || isFunctionExpression) {
  38. return handler.value
  39. }
  40. /* istanbul ignore if */
  41. if (__WEEX__ && handler.params) {
  42. return genWeexHandler(handler.params, handler.value)
  43. }
  44. return `function($event){${
  45. isFunctionInvocation ? `return ${handler.value}` : handler.value
  46. }}` // inline statement
  47. } else { // 对于有modifiers的情况则对各种不同modifer情况做不同处理,添加相应的代码串
  48. let code = ''
  49. let genModifierCode = ''
  50. const keys = []
  51. for (const key in handler.modifiers) {
  52. if (modifierCode[key]) {
  53. genModifierCode += modifierCode[key]
  54. // left/right
  55. if (keyCodes[key]) {
  56. keys.push(key)
  57. }
  58. } else if (key === 'exact') {
  59. const modifiers: ASTModifiers = (handler.modifiers: any)
  60. genModifierCode += genGuard(
  61. ['ctrl', 'shift', 'alt', 'meta']
  62. .filter(keyModifier => !modifiers[keyModifier])
  63. .map(keyModifier => `$event.${keyModifier}Key`)
  64. .join('||')
  65. )
  66. } else {
  67. keys.push(key)
  68. }
  69. }
  70. if (keys.length) {
  71. code += genKeyFilter(keys)
  72. }
  73. // Make sure modifiers like prevent and stop get executed after key filtering
  74. if (genModifierCode) {
  75. code += genModifierCode
  76. }
  77. const handlerCode = isMethodPath
  78. ? `return ${handler.value}.apply(null, arguments)`
  79. : isFunctionExpression
  80. ? `return (${handler.value}).apply(null, arguments)`
  81. : isFunctionInvocation
  82. ? `return ${handler.value}`
  83. : handler.value
  84. /* istanbul ignore if */
  85. if (__WEEX__ && handler.params) {
  86. return genWeexHandler(handler.params, code + handlerCode)
  87. }
  88. return `function($event){${code}${handlerCode}}`
  89. }
  90. }

对于例子而言父组件生成的data串为

  1. {
  2. on: {"select": selectHandler},
  3. nativeOn: {"click": function($event) {
  4. $event.preventDefault();
  5. return clickHandler($event)
  6. }
  7. }
  8. }

子组件生成的data串为

  1. {
  2. on: {"click": function($event) {
  3. clickHandler($event)
  4. }
  5. }
  6. }

编译部分完了,接下来看运行时部分是如何实现的
Vue 的事件有 2 种,一种是原生 DOM 事件,一种是用户自定义事件

DOM事件

之前在 patch 的时候执行各种 module 的钩子函数时这部分是略过的,只分析了 DOM 是如何渲染的,而 DOM 元素相关的属性、样式、事件等都是通过这些 module 的钩子函数完成设置的
所有和 web 相关的 module 都定义在 src/platforms/web/runtime/modules 目录下,这次只关注目录下的 events.js 即可
在 patch 过程中的创建阶段和更新阶段都会执行 updateDOMListeners

  1. let target: any
  2. function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  3. if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
  4. return
  5. }
  6. // vnode.data.on - data中对应的事件对象
  7. const on = vnode.data.on || {}
  8. const oldOn = oldVnode.data.on || {}
  9. // vnode is empty when removing all listeners,
  10. // and use old vnode dom element
  11. // 当前vnode对应的DOM对象
  12. target = vnode.elm || oldVnode.elm
  13. // 对v-model相关的处理
  14. normalizeEvents(on)
  15. updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  16. target = undefined
  17. }

updateListeners定义在src/core/vdom/helpers/update-listeners.js中

  1. // add、remove方法都是外部传入的,因为它既处理原生DOM事件的添加删除,也处理自定义事件的添加删除
  2. export function updateListeners (
  3. on: Object,
  4. oldOn: Object,
  5. add: Function,
  6. remove: Function,
  7. createOnceHandler: Function,
  8. vm: Component
  9. ) {
  10. let name, def, cur, old, event
  11. // 遍历on去添加事件监听
  12. for (name in on) {
  13. def = cur = on[name]
  14. old = oldOn[name]
  15. event = normalizeEvent(name) // 对事件名做处理
  16. /* istanbul ignore if */
  17. if (__WEEX__ && isPlainObject(def)) {
  18. cur = def.handler
  19. event.params = def.params
  20. }
  21. if (isUndef(cur)) {
  22. process.env.NODE_ENV !== 'production' && warn(
  23. `Invalid handler for event "${event.name}": got ` + String(cur),
  24. vm
  25. )
  26. } else if (isUndef(old)) {
  27. if (isUndef(cur.fns)) {
  28. cur = on[name] = createFnInvoker(cur, vm) // 创建一个回调函数
  29. }
  30. if (isTrue(event.once)) {
  31. cur = on[name] = createOnceHandler(event.name, cur, event.capture)
  32. }
  33. add(event.name, cur, event.capture, event.passive, event.params) // 完成一次事件绑定
  34. } else if (cur !== old) { // 当第二次执行该函数的时候,判断cur !== old
  35. old.fns = cur // 更改 old.fns = cur 把之前绑定的 involer.fns 赋值为新的回调函数
  36. on[name] = old // 保留引用关系,这样就保证了事件回调只添加一次,之后仅仅去修改它的回调函数的引用
  37. }
  38. }
  39. // 遍历oldOn去移除事件监听
  40. for (name in oldOn) {
  41. if (isUndef(on[name])) {
  42. event = normalizeEvent(name)
  43. remove(event.name, oldOn[name], event.capture)
  44. }
  45. }
  46. }

normalizeEvent

  1. const normalizeEvent = cached((name: string): {
  2. name: string,
  3. once: boolean,
  4. capture: boolean,
  5. passive: boolean,
  6. handler?: Function,
  7. params?: Array<any>
  8. } => {
  9. // 根据事件名的一些特殊标识(addHandler时添加的)区分事件是否有once、capture、passive修饰符
  10. const passive = name.charAt(0) === '&'
  11. name = passive ? name.slice(1) : name
  12. const once = name.charAt(0) === '~' // Prefixed last, checked first
  13. name = once ? name.slice(1) : name
  14. const capture = name.charAt(0) === '!'
  15. name = capture ? name.slice(1) : name
  16. return {
  17. name,
  18. once,
  19. capture,
  20. passive
  21. }
  22. })

createFnInvoker

  1. export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
  2. // 定义了invoker方法并返回
  3. function invoker () {
  4. const fns = invoker.fns
  5. // 由于一个事件可能会对应多个回调函数,所以做了数组的判断,多个回调函数就依次调用
  6. if (Array.isArray(fns)) {
  7. const cloned = fns.slice()
  8. for (let i = 0; i < cloned.length; i++) {
  9. invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
  10. }
  11. } else {
  12. // return handler return value for single handlers
  13. return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
  14. }
  15. }
  16. // 每一次执行invoker函数都是从invoker.fns里取执行的回调函数
  17. invoker.fns = fns
  18. return invoker
  19. }

在原生DOM事件中真正添加回调和移除回调函数的实现
定义在 src/platforms/web/runtime/modules/event.js 中

  1. function add (
  2. name: string,
  3. handler: Function,
  4. capture: boolean,
  5. passive: boolean
  6. ) {
  7. // async edge case #6566: inner click event triggers patch, event handler
  8. // attached to outer element during patch, and triggered again. This
  9. // happens because browsers fire microtask ticks between event propagation.
  10. // the solution is simple: we save the timestamp when a handler is attached,
  11. // and the handler would only fire if the event passed to it was fired
  12. // AFTER it was attached.
  13. if (useMicrotaskFix) {
  14. const attachedTimestamp = currentFlushTimestamp
  15. const original = handler
  16. handler = original._wrapper = function (e) {
  17. if (
  18. // no bubbling, should always fire.
  19. // this is just a safety net in case event.timeStamp is unreliable in
  20. // certain weird environments...
  21. e.target === e.currentTarget ||
  22. // event is fired after handler attachment
  23. e.timeStamp >= attachedTimestamp ||
  24. // bail for environments that have buggy event.timeStamp implementations
  25. // #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
  26. // #9681 QtWebEngine event.timeStamp is negative value
  27. e.timeStamp <= 0 ||
  28. // #9448 bail if event is fired in another document in a multi-page
  29. // electron/nw.js app, since event.timeStamp will be using a different
  30. // starting reference
  31. e.target.ownerDocument !== document
  32. ) {
  33. return original.apply(this, arguments)
  34. }
  35. }
  36. }
  37. target.addEventListener(
  38. name,
  39. handler,
  40. supportsPassive
  41. ? { capture, passive }
  42. : capture
  43. )
  44. }
  45. function remove (
  46. name: string,
  47. handler: Function,
  48. capture: boolean,
  49. _target?: HTMLElement
  50. ) {
  51. (_target || target).removeEventListener(
  52. name,
  53. handler._wrapper || handler,
  54. capture
  55. )
  56. }

实际上调用原生 addEventListener 和 removeEventListener,并根据参数传递一些配置

自定义事件

自定义事件只能作用在组件上,如果在组件上使用原生事件,需要加 .native 修饰符,普通元素上使用 .native 修饰符无效
在 render 阶段,如果是一个组件节点,则通过 createComponent 创建一个组件 vnode
定义在src/core/vdom/create-component.js中

  1. export function createComponent (
  2. Ctor: Class<Component> | Function | Object | void,
  3. data: ?VNodeData,
  4. context: Component,
  5. children: ?Array<VNode>,
  6. tag?: string
  7. ): VNode | Array<VNode> | void {
  8. // ...
  9. // extract listeners, since these needs to be treated as
  10. // child component listeners instead of DOM listeners
  11. const listeners = data.on
  12. // replace with listeners with .native modifier
  13. // so it gets processed during parent component patch.
  14. data.on = data.nativeOn
  15. // ...
  16. // return a placeholder vnode
  17. const name = Ctor.options.name || tag
  18. // 把listeners作为vnode的componentOptions传入,它是在子组件初始化阶段中处理的,所以它的处理环境是子组件
  19. const vnode = new VNode(
  20. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  21. data, undefined, undefined, undefined, context,
  22. { Ctor, propsData, listeners, tag, children },
  23. asyncFactory
  24. )
  25. // ...
  26. return vnode
  27. }

在子组件的初始化的时候,会执行initInternalComponent方法
定义在 src/core/instance/init.js 中

  1. export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  2. const opts = vm.$options = Object.create(vm.constructor.options)
  3. // doing this because it's faster than dynamic enumeration.
  4. const parentVnode = options._parentVnode
  5. opts.parent = options.parent
  6. opts._parentVnode = parentVnode
  7. const vnodeComponentOptions = parentVnode.componentOptions
  8. opts.propsData = vnodeComponentOptions.propsData
  9. // 拿到父组件传入的listeners
  10. opts._parentListeners = vnodeComponentOptions.listeners
  11. opts._renderChildren = vnodeComponentOptions.children
  12. opts._componentTag = vnodeComponentOptions.tag
  13. if (options.render) {
  14. opts.render = options.render
  15. opts.staticRenderFns = options.staticRenderFns
  16. }
  17. }

在执行 initEvents 的过程中,会处理这个 listeners
定义在 src/core/instance/events.js 中

  1. export function initEvents (vm: Component) {
  2. vm._events = Object.create(null)
  3. vm._hasHookEvent = false
  4. // init parent attached events
  5. const listeners = vm.$options._parentListeners
  6. // 拿到listeners后
  7. if (listeners) {
  8. updateComponentListeners(vm, listeners)
  9. }
  10. }

updateComponentListeners

  1. let target: any
  2. export function updateComponentListeners (
  3. vm: Component,
  4. listeners: Object,
  5. oldListeners: ?Object
  6. ) {
  7. target = vm
  8. // 对于自定义事件和原生DOM事件处理的差异就在事件添加和删除的实现上
  9. updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  10. target = undefined
  11. }

自定义事件add和remove

  1. // 实际上是利用Vue定义的事件中心
  2. function add (event, fn) {
  3. target.$on(event, fn)
  4. }
  5. function remove (event, fn) {
  6. target.$off(event, fn)
  7. }

实现

  1. // 非常经典的事件中心的实现,把所有的事件用 vm._events 存储起来
  2. export function eventsMixin (Vue: Class<Component>) {
  3. const hookRE = /^hook:/
  4. Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  5. const vm: Component = this
  6. if (Array.isArray(event)) {
  7. for (let i = 0, l = event.length; i < l; i++) {
  8. vm.$on(event[i], fn)
  9. }
  10. } else {
  11. (vm._events[event] || (vm._events[event] = [])).push(fn) // 按事件名称把回调函数fn存储起来
  12. // optimize hook:event cost by using a boolean flag marked at registration
  13. // instead of a hash lookup
  14. if (hookRE.test(event)) {
  15. vm._hasHookEvent = true
  16. }
  17. }
  18. return vm
  19. }
  20. // 内部执行vm.$on,并且当回调函数执行一次后再通过vm.$off移除事件的回调。这样来确保回调函数只执行一次
  21. Vue.prototype.$once = function (event: string, fn: Function): Component {
  22. const vm: Component = this
  23. function on () {
  24. vm.$off(event, on)
  25. fn.apply(vm, arguments)
  26. }
  27. on.fn = fn
  28. vm.$on(event, on)
  29. return vm
  30. }
  31. // 移除指定事件名event和指定的fn
  32. Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
  33. const vm: Component = this
  34. // all
  35. if (!arguments.length) {
  36. vm._events = Object.create(null)
  37. return vm
  38. }
  39. // array of events
  40. if (Array.isArray(event)) {
  41. for (let i = 0, l = event.length; i < l; i++) {
  42. vm.$off(event[i], fn)
  43. }
  44. return vm
  45. }
  46. // specific event
  47. const cbs = vm._events[event]
  48. if (!cbs) {
  49. return vm
  50. }
  51. if (!fn) {
  52. vm._events[event] = null
  53. return vm
  54. }
  55. // specific handler
  56. let cb
  57. let i = cbs.length
  58. while (i--) {
  59. cb = cbs[i]
  60. if (cb === fn || cb.fn === fn) {
  61. cbs.splice(i, 1)
  62. break
  63. }
  64. }
  65. return vm
  66. }
  67. Vue.prototype.$emit = function (event: string): Component {
  68. const vm: Component = this
  69. if (process.env.NODE_ENV !== 'production') {
  70. const lowerCaseEvent = event.toLowerCase()
  71. if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
  72. tip(
  73. `Event "${lowerCaseEvent}" is emitted in component ` +
  74. `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
  75. `Note that HTML attributes are case-insensitive and you cannot use ` +
  76. `v-on to listen to camelCase events when using in-DOM templates. ` +
  77. `You should probably use "${hyphenate(event)}" instead of "${event}".`
  78. )
  79. }
  80. }
  81. // 根据事件名event找到所有回调函数
  82. let cbs = vm._events[event]
  83. if (cbs) {
  84. cbs = cbs.length > 1 ? toArray(cbs) : cbs
  85. const args = toArray(arguments, 1)
  86. const info = `event handler for "${event}"`
  87. // 遍历执行回调函数
  88. for (let i = 0, l = cbs.length; i < l; i++) {
  89. invokeWithErrorHandling(cbs[i], vm, args, vm, info)
  90. }
  91. }
  92. return vm
  93. }
  94. }

对于用户自定义的事件添加和删除就是利用了这几个事件中心的 API
需要注意的一点,vm.$emit 是给当前的 vm 上派发的实例,之所以常用它做父子组件通讯,是因为它的回调函数的定义是在父组件中
对于例子而言,当子组件的 button 被点击了,它通过 this.$emit(‘select’) 派发事件,那么子组件的实例就监听到了这个 select 事件,并执行它的回调函数——定义在父组件中的 selectHandler 方法,这样就相当于完成了一次父子组件的通讯

Vue 支持 2 种事件类型,原生 DOM 事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。另外要注意一点,只有组件节点才可以添加自定义事件,并且添加原生 DOM 事件需要使用 native 修饰符;而普通元素使用 .native 修饰符是没有作用的,也只能添加原生 DOM 事件