9.4. 自定义事件

Vue如何处理原生的Dom事件基本流程已经讲完,然而针对事件还有一个重要的概念不可忽略,那就是组件的自定义事件。我们知道父子组件可以利用事件进行通信,子组件通过vm.$emit向父组件分发事件,父组件通过v-on:(event)接收信息并处理回调。因此针对自定义事件在源码中自然有不同的处理逻辑。我们先通过简单的例子展开。

  1. <script>
  2. var child = {
  3. template: `<div @click="emitToParent">点击传递信息给父组件</div>`,
  4. methods: {
  5. emitToParent() {
  6. this.$emit('myevent', 1)
  7. }
  8. }
  9. }
  10. new Vue({
  11. el: '#app',
  12. components: {
  13. child
  14. },
  15. template: `<div id="app"><child @myevent="myevent" @click.native="nativeClick"></child></div>`,
  16. methods: {
  17. myevent(num) {
  18. console.log(num)
  19. },
  20. nativeClick() {
  21. console.log('nativeClick')
  22. }
  23. }
  24. })
  25. </script>

从例子中可以看出,普通节点只能使用原生DOM事件,而组件上却可以使用自定义的事件和原生的DOM事件,并且通过native修饰符区分,有了原生DOM对于事件处理的基础,接下来我们看看自定义事件有什么特别之处。

9.4.1 模板编译

回过头来看看事件的模板编译,在生成AST树阶段,之前分析说过addHandler方法会对事件的修饰符做不同的处理,当遇到native修饰符时,事件相关属性方法会添加到nativeEvents属性中。下图是child生成的AST树:

9.4. 自定义事件 - 图1

9.4.2 代码生成

不管是组件还是普通标签,事件处理代码都在genData的过程中,和之前分析原生事件一致,genHandlers用来处理事件对象并拼接成字符串。

  1. function genData() {
  2. ···
  3. if (el.events) {
  4. data += (genHandlers(el.events, false)) + ",";
  5. }
  6. if (el.nativeEvents) {
  7. data += (genHandlers(el.nativeEvents, true)) + ",";
  8. }
  9. }

getHandlers的逻辑前面已经讲过,处理组件原生事件和自定义事件的区别在isNative选项上,我们看最终生成的代码为:

  1. with(this){return _c('div',{attrs:{"id":"app"}},[_c('child',{on:{"myevent":myevent},nativeOn:{"click":function($event){return nativeClick($event)}}})],1)}

有了render函数接下来会根据它创建Vnode实例,其中遇到组件占位符节点时会创建子组件Vnode, 此时为on,nativeOn做了一层特殊的转换,将nativeOn赋值给on,这样后续的处理方式和普通节点一致。另外,将on赋值给listeners,在创建VNode时以组件配置componentOptions传入。

  1. // 创建子组件过程
  2. function createComponent (){
  3. ···
  4. var listeners = data.on;
  5. // replace with listeners with .native modifier
  6. // so it gets processed during parent component patch.
  7. data.on = data.nativeOn;
  8. ···
  9. var vnode = new VNode(
  10. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  11. data, undefined, undefined, undefined, context,
  12. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  13. asyncFactory
  14. );
  15. return vnode
  16. }

9.4.3 子组件实例

接下来是通过Vnode生成真实节点的过程,这个过程遇到子Vnode会实例化子组件实例。实例化子类构造器的过程又回到之前文章分析的初始化选项配置的过程,在系列最开始的时候分析Vue.prototype.init的过程,跳过了组件初始化的流程,其中针对自定义事件的处理的关键如下

  1. Vue.prototype._init = function(options) {
  2. ···
  3. // 针对子组件的事件处理逻辑
  4. if (options && options._isComponent) {
  5. // 初始化内部组件
  6. initInternalComponent(vm, options);
  7. } else {
  8. // 选项合并,将合并后的选项赋值给实例的$options属性
  9. vm.$options = mergeOptions(
  10. resolveConstructorOptions(vm.constructor),
  11. options || {},
  12. vm
  13. );
  14. }
  15. // 初始化事件处理
  16. initEvents(vm);
  17. }
  18. function initInternalComponent (vm, options) {
  19. var opts = vm.$options = Object.create(vm.constructor.options);
  20. ···
  21. opts._parentListeners = vnodeComponentOptions.listeners;
  22. ···
  23. }

此时,子组件拿到了父占位符节点定义的@myevent="myevent"事件。接下来进行子组件的初始化事件处理,此时vm.$options._parentListeners会拿到父组件自定义的事件。而带有自定义事件的组件会执行updateComponentListeners函数。

  1. function initEvents (vm) {
  2. vm._events = Object.create(null);
  3. vm._hasHookEvent = false;
  4. // init parent attached events
  5. var listeners = vm.$options._parentListeners;
  6. if (listeners) {
  7. // 带有自定义事件属性的实例
  8. updateComponentListeners(vm, listeners);
  9. }
  10. }

之后又回到了之前分析的updateListeners过程,和原生DOM事件不同的是,自定义事件的添加移除的方法不同。

  1. var target = vm;
  2. function add (event, fn) {
  3. target.$on(event, fn);
  4. }
  5. function remove$1 (event, fn) {
  6. target.$off(event, fn);
  7. }
  8. function updateComponentListeners (vm,listeners,oldListeners) {
  9. target = vm;
  10. updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
  11. target = undefined;
  12. }

9.4.4 事件API

我们回头来看看Vue在引入阶段对事件的处理还做了哪些初始化操作。Vue在实例上用一个_events属性存贮管理事件的派发和更新,暴露出$on, $once, $off, $emit方法给外部管理事件和派发执行事件。

  1. eventsMixin(Vue); // 定义事件相关函数
  2. function eventsMixin (Vue) {
  3. var hookRE = /^hook:/;
  4. // $on方法用来监听事件,执行回调
  5. Vue.prototype.$on = function (event, fn) {
  6. var vm = this;
  7. // event支持数组形式。
  8. if (Array.isArray(event)) {
  9. for (var i = 0, l = event.length; i < l; i++) {
  10. vm.$on(event[i], fn);
  11. }
  12. } else {
  13. // _events数组中记录需要监听的事件以及事件触发的回调
  14. (vm._events[event] || (vm._events[event] = [])).push(fn);
  15. if (hookRE.test(event)) {
  16. vm._hasHookEvent = true;
  17. }
  18. }
  19. return vm
  20. };
  21. // $once方法用来监听一次事件,执行回调
  22. Vue.prototype.$once = function (event, fn) {
  23. var vm = this;
  24. // 对fn做一层包装,先解除绑定再执行fn回调
  25. function on () {
  26. vm.$off(event, on);
  27. fn.apply(vm, arguments);
  28. }
  29. on.fn = fn;
  30. vm.$on(event, on);
  31. return vm
  32. };
  33. // $off方法用来解除事件监听
  34. Vue.prototype.$off = function (event, fn) {
  35. var vm = this;
  36. // 如果$off方法没有传递任何参数时,将_events属性清空。
  37. if (!arguments.length) {
  38. vm._events = Object.create(null);
  39. return vm
  40. }
  41. // 数组处理
  42. if (Array.isArray(event)) {
  43. for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
  44. vm.$off(event[i$1], fn);
  45. }
  46. return vm
  47. }
  48. var cbs = vm._events[event];
  49. if (!cbs) {
  50. return vm
  51. }
  52. if (!fn) {
  53. vm._events[event] = null;
  54. return vm
  55. }
  56. // specific handler
  57. var cb;
  58. var i = cbs.length;
  59. while (i--) {
  60. cb = cbs[i];
  61. if (cb === fn || cb.fn === fn) {
  62. // 将监听的事件回调移除
  63. cbs.splice(i, 1);
  64. break
  65. }
  66. }
  67. return vm
  68. };
  69. // $emit方法用来触发事件,执行回调
  70. Vue.prototype.$emit = function (event) {
  71. var vm = this;
  72. {
  73. var lowerCaseEvent = event.toLowerCase();
  74. if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
  75. tip(
  76. "Event \"" + lowerCaseEvent + "\" is emitted in component " +
  77. (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
  78. "Note that HTML attributes are case-insensitive and you cannot use " +
  79. "v-on to listen to camelCase events when using in-DOM templates. " +
  80. "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
  81. );
  82. }
  83. }
  84. var cbs = vm._events[event];
  85. // 找到已经监听事件的回调,执行
  86. if (cbs) {
  87. cbs = cbs.length > 1 ? toArray(cbs) : cbs;
  88. var args = toArray(arguments, 1);
  89. var info = "event handler for \"" + event + "\"";
  90. for (var i = 0, l = cbs.length; i < l; i++) {
  91. invokeWithErrorHandling(cbs[i], vm, args, vm, info);
  92. }
  93. }
  94. return vm
  95. };
  96. }

有了这些事件api,自定义事件的添加移除理解起来也简单很多。组件通过this.$emit在组件实例中派发了事件,而在这之前,组件已经将需要监听的事件以及回调添加到实例的_events属性中,触发事件时便可以直接执行监听事件的回调。

最后,我们换一个角度理解父子组件通信,组件自定义事件的触发和监听本质上都是在当前的组件实例中进行,之所以能产生父子组件通信的效果是因为事件监听的回调函数写在了父组件中。