• Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解


    我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现

    组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事件需要加一个.native修饰符。

    子组件里通过过this.$emit()将自定义事件以及需要发出的数据通过以下代码发送出去,第一个参数是自定义事件的名称,后面的参数是依次想要发送出去的数据,例如:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
        <title>Document</title>    
    </head>
    <body>
        <div id="d"><com @myclick="MyClick" @mouseenter.native="Enter"></com></div>
        <script>
    Vue.config.productionTip=false; Vue.config.devtools=false; Vue.component('com',{ template:'<button @click="childclick">Click</button>', methods:{ childclick:function(){this.$emit('myclick','gege','123')} //子组件的事件,通过this.$emit触发父组件的myclick事件 } }) debugger var app = new Vue({ el:'#d', methods:{ MyClick:function(){console.log('parent MyClick method:',arguments)}, //响应子组件的事件函数 Enter:function(){console.log("MouseEnter")} //子组件的原生DOM事件 } }) </script> </body> </html>

    子组件就是一个按钮,渲染如下:

    我们给整个组件绑定了两个事件,一个DOM原生的mouseenter事件和自定义的MyClick组件事件,当鼠标移动到按钮上时,打印出:MouseEnter,如下:

    当点击按钮时输出子组件传递过来的信息,如下:

    自定义事件其实是存储在组件实例的_events属性上的,我们在控制台输入console.log(app.$children[0]["_events"])就可以打印出来,如下:

    myclick就是我们自定义的事件对象

    writer by:大沙漠 QQ:22969969

     源码分析


     父组件在解析模板时会执行processAttrs()函数,会在AST对象上增加一个events和nativeevents属性,如下

    function processAttrs (el) {      //第9526行 对属性进行解析
      var list = el.attrsList; 
      var i, l, name, rawName, value, modifiers, isProp;
      for (i = 0, l = list.length; i < l; i++) {              //遍历每个属性名
        name = rawName = list[i].name;
        value = list[i].value;
        if (dirRE.test(name)) {
          // mark element as dynamic
          el.hasBindings = true;
          // modifiers
          modifiers = parseModifiers(name);
          if (modifiers) {
            name = name.replace(modifierRE, '');
          }
          if (bindRE.test(name)) { // v-bind
            /**/
          } else if (onRE.test(name)) { // v-on                   //如果name以@或v-on:开头,表示绑定了事件
            name = name.replace(onRE, '');
            addHandler(el, name, value, modifiers, false, warn$2);    调用addHandler()函数将事件相关信息保存到el.events或nativeEvents里面
          } else { // normal directives
            /**/
          }
        } else {
          /**/
        }
      }
    }
    
    function addHandler (                //第6573行 给el这个AST对象增加event或nativeEvents
      el,
      name,
      value,
      modifiers,
      important,
      warn
    ) {
      modifiers = modifiers || emptyObject;
      /**/
    
      var events;
      if (modifiers.native) {                       //如果存在native修饰符,则保存到el.nativeEvents里面
        delete modifiers.native;
        events = el.nativeEvents || (el.nativeEvents = {});
      } else {                                      //否则保存到el.events里面
        events = el.events || (el.events = {});
      }
    
      /**/
      var handlers = events[name];                  //尝试获取已经存在的该事件对象
      /* istanbul ignore if */ 
      if (Array.isArray(handlers)) {                //如果是数组,表示已经插入了两次了,则再把newHandler添加进去
        important ? handlers.unshift(newHandler) : handlers.push(newHandler);
      } else if (handlers) {                        //如果handlers存在且不是数组,则表示只插入过一次,则把events[name]变为数组
        events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
      } else {                                      //否则表示是第一次新增该事件,则值为对应的newHandler
        events[name] = newHandler;
      }
    
      el.plain = false;
    }

    例子里执行完后AST对象里对应的信息如下:(AST可以这样认为:Vue把模板通过正则解析后以对象的形式表现出来)

    接下来在generate生成rendre函数的时候会调用genHandlers函数根据不同修饰符等生成对应的属性(作为_c函数的第二个data参数一部分),如下:

    function genData$2 (el, state) {  //第10274行  拼凑data值
      var data = '{'; 
    
      /**/
      // event handlers
      if (el.events) {               //如果el有绑定事件(没有native修饰符时)
        data += (genHandlers(el.events, false, state.warn)) + ",";
      }
      if (el.nativeEvents) {        //如果el有绑定事件(native修饰符时)
        data += (genHandlers(el.nativeEvents, true, state.warn)) + ",";
      }
      /**/
      return data
    }

    genHandlers会根据参数2的值将事件存储在nativeOn或on属性里,如下:

    function genHandlers (      //第9992行 拼凑事件的data函数
      events,
      isNative,
      warn
    ) {
      var res = isNative ? 'nativeOn:{' : 'on:{';       //如果参数isNative为true则设置res为:nativeOn:{,否则为:on:{
      for (var name in events) {
        res += """ + name + "":" + (genHandler(name, events[name])) + ",";
      }
      return res.slice(0, -1) + '}'
    }

    例子里执行到这里时等于:

     _render将rendre函数转换为VNode时候会调用createComponent()函数创建组件占位符VNode,此时会有

    function createComponent (  //第4182行
      Ctor, 
      data,
      context,
      children,
      tag
    ) {
      /**/
      var listeners = data.on;          //对自定义事件(没有native修饰符)的处理,则保存到listeners里面,一会儿存到占位符VNode的配置信息里
      // replace with listeners with .native modifier
      // so it gets processed during parent component patch.
      data.on = data.nativeOn;          //对原生DOM事件,则保存到data.on里面,这样等该DOM渲染成功后会执行event模块的初始化,就会绑定对应的函数了
    
      /**/
      var name = Ctor.options.name || tag;
      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 },        //自定义事件作为listeners属性存储在组件Vnode的配置参数里了
        asyncFactory
      );

      // Weex specific: invoke recycle-list optimized @render function for
      // extracting cell-slot template.
      // https://github.com/Hanks10100/weex-native-directive/tree/master/component
      /* istanbul ignore if */
      return vnode
    }

    原生事件存储在on属性上,后面介绍v-on指令时再详细介绍,对于自定义事件存储在组件Vnode配置参数的listeners属性里了。

    当组件实例化的时候执行_init()时首先执行initInternalComponent()函数,该函数会获取listeners属性,如下:

    function initInternalComponent (vm, options) {        //第4632行  初始化子组件
      var opts = vm.$options = Object.create(vm.constructor.options);
      // doing this because it's faster than dynamic enumeration.
      var parentVnode = options._parentVnode;                          //该组件的占位符VNode
      opts.parent = options.parent;
      opts._parentVnode = parentVnode;
      opts._parentElm = options._parentElm;
      opts._refElm = options._refElm;
    
      var vnodeComponentOptions = parentVnode.componentOptions;         //占位符VNode初始化传入的配置信息
      opts.propsData = vnodeComponentOptions.propsData;
      opts._parentListeners = vnodeComponentOptions.listeners;          //将组件的自定义事件保存到_parentListeners属性里面
      opts._renderChildren = vnodeComponentOptions.children;
      opts._componentTag = vnodeComponentOptions.tag;
    
      if (options.render) {
        opts.render = options.render;
        opts.staticRenderFns = options.staticRenderFns;
      }
    }

     回到_init函数,接着执行initEvents()函数,该函数会初始化组件的自定义事件,如下:

    function initEvents (vm) {      //第2412行 初始化自定义事件
      vm._events = Object.create(null);
      vm._hasHookEvent = false;
      // init parent attached events
      var listeners = vm.$options._parentListeners;       //获取占位符VNode上的自定义事件
      if (listeners) {                                    
        updateComponentListeners(vm, listeners);          //执行updateComponentListeners()新增事件
      }
    }

      updateComponentListeners函数用于新增/更新组件的事件,如下:

    function add (event, fn, once) {      //第2424行
      if (once) {
        target.$once(event, fn);            //自定义事件最终调用$once绑定事件的
      } else {
        target.$on(event, fn);
      }
    }
    
    function remove$1 (event, fn) {
      target.$off(event, fn);
    }
    
    function updateComponentListeners (       //第2436行
      vm,
      listeners,
      oldListeners
    ) {
      target = vm;
      updateListeners(listeners, oldListeners || {}, add, remove$1, vm);    //调用updateListeners()更新DOM事件,传入add函数
      target = undefined;
    }

    updateListeners内部会调用add()函数,这里用了一个优化措施,实际上我们绑定的是Vue内部的createFnInvoker函数,该函数会遍历传给updateListeners的函数,依次执行。

    add()最终执行的是$on()函数,该函数定义如下:

      Vue.prototype.$on = function (event, fn) {  //第2448行 自定义事件的新增  event:函数名 fn:对应的函数
        var this$1 = this; 
    
        var vm = this;
        if (Array.isArray(event)) {                         //如果event是一个数组
          for (var i = 0, l = event.length; i < l; i++) {       //则遍历该数组
            this$1.$on(event[i], fn);                               //依次调用this$1.$on
          }
        } else {                                             //如果不是数组
          (vm._events[event] || (vm._events[event] = [])).push(fn);     //则将事件保存到ev._event上
          // optimize hook:event cost by using a boolean flag marked at registration
          // instead of a hash lookup
          if (hookRE.test(event)) {                                  //如果事件名以hook:开头                    
            vm._hasHookEvent = true;                                    //则设置vm._hasHookEvent为true,这样生命周期函数执行时也会执行这些函数
          }
        }
        return vm
      };

    从这里可以看到自定义事件其实是保存到组件实例的_events属性上的

    当子组件通过$emit触发当前实例上的事件时,会从_events上拿到对应的自定义事件并执行,如下:

      Vue.prototype.$emit = function (event) {  //第2518行  子组件内部通过$emit()函数执行到这里
        var vm = this;
        {
          var lowerCaseEvent = event.toLowerCase();                       //先将事件名转换为小写    
          if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {   //如果lowerCaseEvent不等于event则报错(即事件名只能是小写)
            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];                                      //从_events属性里获取对应的函数数组
        if (cbs) {
          cbs = cbs.length > 1 ? toArray(cbs) : cbs;                        //获取所有函数
          var args = toArray(arguments, 1);                                 //去掉第一个参数,后面的都作为事件的参数
          for (var i = 0, l = cbs.length; i < l; i++) {                     //遍历cbs
            try {
              cbs[i].apply(vm, args);                                           //依次执行每个函数,值为子组件的vm实例
            } catch (e) {
              handleError(e, vm, ("event handler for "" + event + """));
            }
          }
        }
        return vm
      };

    大致流程跑完了,有点繁琐,多调试一下就好了。

  • 相关阅读:
    同样的代码bug
    Vim中的Tab
    在Vue中同时使用过渡和动画
    在Vue中使用animate.css
    Vue中的css动画
    动态组件与v-once指令
    在Vue中使用插槽
    非父子组件间的传值
    给组件绑定原生事件
    组件参数校验和非props特性
  • 原文地址:https://www.cnblogs.com/greatdesert/p/11091402.html
Copyright © 2020-2023  润新知