欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-04-29 18:49:54
我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现 组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事件需要加一个.native修饰符。 子组件里通过过this.$emit()将自定义事件以及需要发出的 ......

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

组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事件需要加一个.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>

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

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

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

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

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

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

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

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

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

 

 源码分析


 父组件在解析模板时会执行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把模板通过正则解析后以对象的形式表现出来)

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

接下来在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) + '}'
}

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

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

 _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
  };

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