这个系列讲到这里,Vue根本外围的货色曾经剖析完,然而Vue之所以弱小,离不开它提供给用户的一些实用功能,开发者能够更偏差于业务逻辑而非基本功能的实现。例如,在日常开发中,咱们将@click=***用得飞起,然而咱们是否思考,Vue如何在前面为咱们的模板做事件相干的解决,并且咱们常常利用组件的自定义事件去实现父子间的通信,那这个事件和和原生dom事件又有不同的中央吗,可能实现通信的原理又是什么,带着纳闷,咱们深刻源码开展剖析。

9.1. 模板编译

Vue在挂载实例前,有相当多的工作是进行模板的编译,将template模板进行编译,解析成AST树,再转换成render函数,而有了render函数后才会进入实例挂载过程。对于事件而言,咱们常常应用v-on或者@在模板上绑定事件。因而对事件的第一步解决,就是在编译阶段对事件指令做收集解决。

从一个简略的用法剖析编译阶段收集的信息:

<div id="app">    <div v-on:click.stop="doThis">点击</div>    <span>{{count}}</span></div><script>var vm = new Vue({    el: '#app',    data() {        return {            count: 1        }    },    methods: {        doThis() {            ++this.count        }    }})</script>

咱们之前在将模板编译的时候大抵说过编译的流程,模板编译的入口是在var ast = parse(template.trim(), options);中,parse通过拆分模板字符串,将其解析为一个AST树,其中对于属性的解决,在processAttr中,因为分支较多,咱们只剖析例子中的流程。

var dirRE = /^v-|^@|^:/;function processAttrs (el) {    var list = el.attrsList;    var i, l, name, rawName, value, modifiers, syncGen, isDynamic;    for (i = 0, l = list.length; i < l; i++) {      name = rawName = list[i].name; // v-on:click      value = list[i].value; // doThis      if (dirRE.test(name)) { // 匹配v-或者@结尾的指令        el.hasBindings = true;        modifiers = parseModifiers(name.replace(dirRE, ''));// parseModifiers('on:click')        if (modifiers) {          name = name.replace(modifierRE, '');        }        if (bindRE.test(name)) { // v-bind分支          // ...留到v-bind指令时剖析        } else if (onRE.test(name)) { // v-on分支          name = name.replace(onRE, ''); // 拿到真正的事件click          isDynamic = dynamicArgRE.test(name);// 动静事件绑定          if (isDynamic) {            name = name.slice(1, -1);          }          addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);        } else { // normal directives         // 其余指令相干逻辑      } else {}    }  }

processAttrs的逻辑尽管较多,然而了解起来较为简单,var dirRE = /^v-|^@|^:/;是匹配事件相干的正则,命中匹配的记功会失去事件指令相干内容,包含事件自身,事件回调以及事件修饰符。最终通过addHandler办法,为AST树增加事件相干的属性。而addHandler还有一个重要性能是对事件修饰符进行非凡解决。

// el是以后解析的AST树function addHandler (el,name,value,modifiers,important,warn,range,dynamic) {    modifiers = modifiers || emptyObject;    // passive 和 prevent不能同时应用,能够参照官网文档阐明    if (      warn &&      modifiers.prevent && modifiers.passive    ) {      warn(        'passive and prevent can\'t be used together. ' +        'Passive handler can\'t prevent default event.',        range      );    }    // 这部分的逻辑会对非凡的修饰符做字符串拼接的解决,以备后续的应用    if (modifiers.right) {      if (dynamic) {        name = "(" + name + ")==='click'?'contextmenu':(" + name + ")";      } else if (name === 'click') {        name = 'contextmenu';        delete modifiers.right;      }    } else if (modifiers.middle) {      if (dynamic) {        name = "(" + name + ")==='click'?'mouseup':(" + name + ")";      } else if (name === 'click') {        name = 'mouseup';      }    }    if (modifiers.capture) {      delete modifiers.capture;      name = prependModifierMarker('!', name, dynamic);    }    if (modifiers.once) {      delete modifiers.once;      name = prependModifierMarker('~', name, dynamic);    }    /* istanbul ignore if */    if (modifiers.passive) {      delete modifiers.passive;      name = prependModifierMarker('&', name, dynamic);    }    // events 用来记录绑定的事件    var events;    if (modifiers.native) {      delete modifiers.native;      events = el.nativeEvents || (el.nativeEvents = {});    } else {      events = el.events || (el.events = {});    }    var newHandler = rangeSetItem({ value: value.trim(), dynamic: dynamic }, range);    if (modifiers !== emptyObject) {      newHandler.modifiers = modifiers;    }    var 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树:

9.2. 代码生成

模板编译的最初一步是依据解析完的AST树生成对应平台的渲染函数,也就是render函数的生成过程, 对应var code = generate(ast, options);

function generate (ast,options) {    var state = new CodegenState(options);    var code = ast ? genElement(ast, state) : '_c("div")';    return {      render: ("with(this){return " + code + "}"), // with函数      staticRenderFns: state.staticRenderFns    }  }

其中外围解决在getElement中,getElement函数会依据不同指令类型解决不同的分支,对于一般模板的编译会进入genData函数中解决,同样剖析只针对事件相干的解决,从后面解析出的AST树显著看出,AST树中多了events的属性,genHandlers函数会为event属性做逻辑解决。

function genData (el, state) {    var data = '{';    // directives first.    // directives may mutate the el's other properties before they are generated.    var dirs = genDirectives(el, state);    if (dirs) { data += dirs + ','; }    //其余解决    ···    // event handlers    if (el.events) {      data += (genHandlers(el.events, false)) + ",";    }    ···    return data  }

genHandlers的逻辑,会遍历解析好的AST树,拿到event对象属性,并依据属性上的事件对象拼接成字符串。参考Vue3源码视频解说:进入学习

function genHandlers (events,isNative) {    var prefix = isNative ? 'nativeOn:' : 'on:';    var staticHandlers = "";    var dynamicHandlers = "";    // 遍历ast树解析好的event对象    for (var name in events) {      //genHandler实质上是将事件对象转换成可拼接的字符串      var 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    }  }// 事件模板书写匹配var isMethodPath = simplePathRE.test(handler.value); // doThisvar isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)function genHandler (handler) {    if (!handler) {      return 'function(){}'    }    // 事件绑定能够多个,多个在解析ast树时会以数组的模式存在,如果有多个则会递归调用getHandler办法返回数组。    if (Array.isArray(handler)) {      return ("[" + (handler.map(function (handler) { return genHandler(handler); }).join(',')) + "]")    }    // value: doThis 能够有三种形式    var isMethodPath = simplePathRE.test(handler.value); // doThis    var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}    var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)    // 没有任何修饰符    if (!handler.modifiers) {      // 合乎函数定义标准,则间接返回调用函数名 doThis      if (isMethodPath || isFunctionExpression) {        return handler.value      }      // 不合乎则通过function函数封装返回      return ("function($event){" + (isFunctionInvocation ? ("return " + (handler.value)) : handler.value) + "}") // inline statement    } else {    // 蕴含修饰符的场景    }  }

模板中事件的写法有三种,别离对应上诉上个正则匹配的内容。

    1. <div @click="doThis"></div>
    1. <div @click="doThis($event)"></div>
    1. <div @click="()=>{}"></div> <div @click="function(){}"></div>

上述对事件对象的转换,如果事件不带任何修饰符,并且满足正确的模板写法,则间接返回调用事件名,如果不满足,则有可能是<div @click="console.log(11)"></div>的写法,此时会封装到function($event){}中。

蕴含修饰符的场景较多,咱们独自列出剖析。以上文中的例子阐明,modifiers: { stop: true }会拿到stop对应须要增加的逻辑脚本'$event.stopPropagation();',并将它增加到函数字符串中返回。

function genHandler() {  // ···  } else {    var code = '';    var genModifierCode = '';    var keys = [];    // 遍历modifiers上记录的修饰符    for (var key in handler.modifiers) {      if (modifierCode[key]) {        // 依据修饰符增加对应js的代码        genModifierCode += modifierCode[key];        // left/right        if (keyCodes[key]) {          keys.push(key);        }        // 针对exact的解决      } else if (key === 'exact') {        var modifiers = (handler.modifiers);        genModifierCode += genGuard(          ['ctrl', 'shift', 'alt', 'meta']            .filter(function (keyModifier) { return !modifiers[keyModifier]; })            .map(function (keyModifier) { return ("$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 filtering    if (genModifierCode) {      code += genModifierCode;    }    // 依据三种不同的书写模板返回不同的字符串    var handlerCode = isMethodPath      ? ("return " + (handler.value) + "($event)")      : isFunctionExpression        ? ("return (" + (handler.value) + ")($event)")        : isFunctionInvocation          ? ("return " + (handler.value))          : handler.value;    return ("function($event){" + code + handlerCode + "}")  }}var modifierCode = {  stop: '$event.stopPropagation();',  prevent: '$event.preventDefault();',  self: genGuard("$event.target !== $event.currentTarget"),  ctrl: genGuard("!$event.ctrlKey"),  shift: genGuard("!$event.shiftKey"),  alt: genGuard("!$event.altKey"),  meta: genGuard("!$event.metaKey"),  left: genGuard("'button' in $event && $event.button !== 0"),  middle: genGuard("'button' in $event && $event.button !== 1"),  right: genGuard("'button' in $event && $event.button !== 2")};

通过这一转换后,生成with封装的render函数如下:

"_c('div',{attrs:{"id":"app"}},[_c('div',{on:{"click":function($event){$event.stopPropagation();return doThis($event)}}},[_v("点击")]),_v(" "),_c('span',[_v(_s(count))])])"

9.3. 事件绑定

后面花了大量的篇幅介绍了模板上的事件标记在构建AST树上是怎么解决,并且如何依据构建的AST树返回正确的render渲染函数,然而真正事件绑定还是离不开绑定注册事件。这一个阶段就是产生在组件挂载的阶段。
有了render函数,天然能够生成实例挂载须要的Vnode树,并且会进行patchVnode的环节进行实在节点的构建,如果发现过程曾经忘记,能够回顾以往章节。 Vnode树的构建过程和之前介绍的内容没有显著的区别,所以这个过程就不做赘述,最终生成的vnode如下:

有了Vnode,接下来会遍历子节点递归调用createElm为每个子节点创立实在的DOM,因为Vnode中有data属性,在创立实在DOM时会进行注册相干钩子的过程,其中一个就是注册事件相干解决。

function createElm() {  ···  // 针对指令的解决   if (isDef(data)) {      invokeCreateHooks(vnode, insertedVnodeQueue);    }}function invokeCreateHooks (vnode, insertedVnodeQueue) {  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {    cbs.create[i$1](emptyNode, vnode);  }  i = vnode.data.hook; // Reuse variable  if (isDef(i)) {    if (isDef(i.create)) { i.create(emptyNode, vnode); }    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }  }}var events = {  create: updateDOMListeners,  update: updateDOMListeners};

咱们常常会在template模板中定义v-on事件,v-bind动静属性,v-text动静指令等,和v-on事件指令一样,他们都会在编译阶段和Vnode生成阶段创立data属性,因而invokeCreateHooks就是一个模板指令解决的工作,他别离针对不同的指令为实在阶段创立不同的工作。针对事件,这里会调用updateDOMListeners对实在的DOM节点注册事件工作。

function updateDOMListeners (oldVnode, vnode) {  // on是事件指令的标记  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {    return  }  // 新旧节点不同的事件绑定解绑  var on = vnode.data.on || {};  var oldOn = oldVnode.data.on || {};  // 拿到须要增加事件的实在DOM节点  target$1 = vnode.elm;  // normalizeEvents是对事件兼容性的解决  normalizeEvents(on);  updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);  target$1 = undefined;}

其中normalizeEvents是针对v-model的解决,例如在IE下不反对change事件,只能用input事件代替。

updateListeners的逻辑也很简略,它会遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听,它即要解决原生DOM事件的增加和移除,也要解决自定义事件的增加和移除,对于自定义事件,后续内容再剖析。

function updateListeners (on,oldOn,add,remove$$1,createOnceHandler,vm) {    var name, def$$1, cur, old, event;    // 遍历事件    for (name in on) {      def$$1 = cur = on[name];      old = oldOn[name];      event = normalizeEvent(name);      if (isUndef(cur)) {        // 事件名非法的报错解决        warn(          "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),          vm        );      } else if (isUndef(old)) {        // 旧节点不存在        if (isUndef(cur.fns)) {          // createFunInvoker返回事件最终执行的回调函数          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) {        old.fns = cur;        on[name] = old;      }    }    // 旧节点存在,接触旧节点上的绑定事件    for (name in oldOn) {      if (isUndef(on[name])) {        event = normalizeEvent(name);        remove$$1(event.name, oldOn[name], event.capture);      }    }  }

在初始构建实例时,旧节点是不存在的,此时会调用createFnInvoker函数对事件回调函数做一层封装,因为单个事件的回调能够有多个,因而createFnInvoker的作用是对单个,多个回调事件对立封装解决,返回一个当事件触发时真正执行的匿名函数。

function createFnInvoker (fns, vm) {  // 当事件触发时,执行invoker办法,办法执行fns  function invoker () {    var arguments$1 = arguments;    var fns = invoker.fns;    // fns是多个回调函数组成的数组    if (Array.isArray(fns)) {      var cloned = fns.slice();      for (var i = 0; i < cloned.length; i++) {        // 遍历执行真正的回调函数        invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");      }    } else {      // return handler return value for single handlers      return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")    }  }  invoker.fns = fns;  // 返回最终事件执行的回调函数  return invoker}

其中invokeWithErrorHandling会执行定义好的回调函数,这里做了同步异步回调的错误处理。try-catch用于同步回调捕捉异样谬误,Promise.catch用于捕捉异步工作返回谬误。

function invokeWithErrorHandling (handler,context,args,vm,info) {    var res;    try {      res = args ? handler.apply(context, args) : handler.call(context);      if (res && !res._isVue && isPromise(res)) {        // issue #9511        // reassign to res to avoid catch triggering multiple times when nested calls        // 当生命周期钩子函数外部执行返回promise对象是,如果捕捉异样,则会对异样信息做一层包装返回        res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });      }    } catch (e) {      handleError(e, vm, info);    }    return res  }

如果事件只触发一次(即应用了once修饰符),则调用createOnceHandler匿名,在执行完回调之后,移除事件绑定。

function createOnceHandler (event, handler, capture) {    var _target = target$1;     return function onceHandler () {      //调用事件回调      var res = handler.apply(null, arguments);      if (res !== null) {        // 移除事件绑定        remove$2(event, onceHandler, capture, _target);      }    }  }

addremove是真正在DOM上绑定事件和解绑事件的过程,它的实现也是利用了原生DOMaddEventListener,removeEventListener api

function add (name,handler,capture,passive){  ···  target$1.addEventListener(name,handler,      supportsPassive        ? { capture: capture, passive: passive }        : capture);}function remove (name,handler,capture,_target) {  (_target || target$1).removeEventListener(    name,    handler._wrapper || handler,    capture  );}

另外事件的解绑除了产生在只触发一次的事件,也产生在组件更新patchVnode过程,具体不开展剖析,能够参考之前介绍组件更新的内容钻研updateListeners的过程。

9.4. 自定义事件

Vue如何解决原生的Dom事件根本流程曾经讲完,然而针对事件还有一个重要的概念不可疏忽,那就是组件的自定义事件。咱们晓得父子组件能够利用事件进行通信,子组件通过vm.$emit向父组件散发事件,父组件通过v-on:(event)接管信息并解决回调。因而针对自定义事件在源码中天然有不同的解决逻辑。咱们先通过简略的例子开展。

<script>    var child = {      template: `<div @click="emitToParent">点击传递信息给父组件</div>`,      methods: {        emitToParent() {          this.$emit('myevent', 1)        }      }    }    new Vue({      el: '#app',      components: {        child      },      template: `<div id="app"><child @myevent="myevent" @click.native="nativeClick"></child></div>`,      methods: {        myevent(num) {          console.log(num)        },        nativeClick() {          console.log('nativeClick')        }      }    })  </script>

从例子中能够看出,一般节点只能应用原生DOM事件,而组件上却能够应用自定义的事件和原生的DOM事件,并且通过native修饰符辨别,有了原生DOM对于事件处理的根底,接下来咱们看看自定义事件有什么特别之处。

9.4.1 模板编译

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

9.4.2 代码生成

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

function genData() {  ···  if (el.events) {    data += (genHandlers(el.events, false)) + ",";  }  if (el.nativeEvents) {    data += (genHandlers(el.nativeEvents, true)) + ",";  }}

getHandlers的逻辑后面曾经讲过,解决组件原生事件和自定义事件的区别在isNative选项上,咱们看最终生成的代码为:

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传入。

 // 创立子组件过程function createComponent (){  ···  var listeners = data.on;  // replace with listeners with .native modifier  // so it gets processed during parent component patch.  data.on = data.nativeOn;  ···  var vnode = new VNode(    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),    data, undefined, undefined, undefined, context,    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },    asyncFactory  );  return vnode}
9.4.3 子组件实例

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

Vue.prototype._init = function(options) {  ···  // 针对子组件的事件处理逻辑  if (options && options._isComponent) {    // 初始化外部组件    initInternalComponent(vm, options);  } else {    // 选项合并,将合并后的选项赋值给实例的$options属性    vm.$options = mergeOptions(      resolveConstructorOptions(vm.constructor),      options || {},      vm    );  }  // 初始化事件处理  initEvents(vm);}function initInternalComponent (vm, options) {  var opts = vm.$options = Object.create(vm.constructor.options);  ···  opts._parentListeners = vnodeComponentOptions.listeners;  ···}

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

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

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

var target = vm;function add (event, fn) {  target.$on(event, fn);}function remove$1 (event, fn) {  target.$off(event, fn);} function updateComponentListeners (vm,listeners,oldListeners) {  target = vm;  updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);  target = undefined;}
9.4.4 事件API

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

  eventsMixin(Vue); // 定义事件相干函数  function eventsMixin (Vue) {    var hookRE = /^hook:/;    // $on办法用来监听事件,执行回调    Vue.prototype.$on = function (event, fn) {      var vm = this;      // event反对数组模式。      if (Array.isArray(event)) {        for (var i = 0, l = event.length; i < l; i++) {          vm.$on(event[i], fn);        }      } else {        // _events数组中记录须要监听的事件以及事件触发的回调        (vm._events[event] || (vm._events[event] = [])).push(fn);        if (hookRE.test(event)) {          vm._hasHookEvent = true;        }      }      return vm    };    // $once办法用来监听一次事件,执行回调    Vue.prototype.$once = function (event, fn) {      var vm = this;      // 对fn做一层包装,先解除绑定再执行fn回调      function on () {        vm.$off(event, on);        fn.apply(vm, arguments);      }      on.fn = fn;      vm.$on(event, on);      return vm    };    // $off办法用来解除事件监听    Vue.prototype.$off = function (event, fn) {      var vm = this;      // 如果$off办法没有传递任何参数时,将_events属性清空。      if (!arguments.length) {        vm._events = Object.create(null);        return vm      }      // 数组解决      if (Array.isArray(event)) {        for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {          vm.$off(event[i$1], fn);        }        return vm      }      var cbs = vm._events[event];      if (!cbs) {        return vm      }      if (!fn) {        vm._events[event] = null;        return vm      }      // specific handler      var cb;      var i = cbs.length;      while (i--) {        cb = cbs[i];        if (cb === fn || cb.fn === fn) {          // 将监听的事件回调移除          cbs.splice(i, 1);          break        }      }      return vm    };    // $emit办法用来触发事件,执行回调    Vue.prototype.$emit = function (event) {      var vm = this;      {        var 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 + "\"."          );        }      }      var cbs = vm._events[event];      // 找到曾经监听事件的回调,执行      if (cbs) {        cbs = cbs.length > 1 ? toArray(cbs) : cbs;        var args = toArray(arguments, 1);        var info = "event handler for \"" + event + "\"";        for (var i = 0, l = cbs.length; i < l; i++) {          invokeWithErrorHandling(cbs[i], vm, args, vm, info);        }      }      return vm    };  }

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

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

9.5 小结

事件是咱们日常开发中必不可少的性能点,Vue在应用层裸露了@,v-on的指令供开发者在模板中绑定事件。事件指令在模板编译阶段会以属性的模式存在,而在实在节点渲染阶段会依据事件属性去绑定相干的事件。对于组件的事件而言,咱们能够利用事件进行子父组件间的通信,他实质上是在同个子组件外部保护了一个事件总线,从剖析后果能够看出,之所以有子父组件通信的成果,起因仅仅是因为回调函数写在了父组件中。