学习vue源码(1) 手写与事件相关的实例方法

在Vue.js内部有这样一段代码

其中定义了Vue构造函数,然后分别调用initMiXin,stateMixin,eventsMixin,lifecycleMixin,renderMixin,并将Vue构造函数当作参数传给这5个函数

这5个函数的作用就是向Vue的原型上挂载方法。

当函数initMixin被调用时,会向Vue构造函数的prototype属性添加_init方法,执行new Vue()时,会调用_init方法。

好,接下来进入到本文的标题,与事件相关的实例方法有4个,分别是,vm. o n , v m . on , vm. on,vm.emit,vm. o n c e , v m . once,vm. once,vm.off;

这四个方法实在eventsMixin中挂在到Vue构造函数和prototype属性中的。

当eventsMixin被调用的时候,会向Vue构造函数的prototype属性添加4个实例方法。

下面将介绍一下这四种方法

用法:监听当前实例上的自定义事件,事件可以由vm.$emit触发,回调函数会接收所有传入事件所触发的函数的额外参数

原理:只需要在注册事件时将回调函数收集起来,在触发事件时将收集起来的回调函数依次调用即可,代码如下

 Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
  };


当event为数组的时候,需要遍历数组,使得数组中的每一项都调用vm.$on,使回调可以注册到数组中每项事件名所指定的事件列表中,当event参数不为数组时,就像事件列表中添加回调。
vm._events是一个对象,用来存储事件,使用事件名(event)从vm._events中取出事件列表,如果列表不存在,则使用空数组初始化,然后将回调函数添加到事件列表中。
vm._events在执行new Vue()时,会执行this._init方法进行一系列操作,其中就在vue.js的实例上创建一个_events

用法:触发当前实例上的事件,附加参数都会传给监听器回调。

原理:vm.$emit作用是触发事件,所有的事件监听器回调都会存储在vm._events中,所以触发事件的实现思路是使用事件名从vm._events中取出对应的事件监听器回调

列表,然后依次执行列表中的监听器回调并将参数传递给监听器回调,代码如下

Vue.prototype.$emit = function (event) {
    var vm = this;
    var cbs = vm._events[event];
    if (cbs) {
    //   const args = toArray(arguments,1);
      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++) {
          try{
              cbs[i].apply(vm,args)
          }catch(e){
              handleError(e,vm,`event handler for "${event}"`)
          }
      }
    }
    return vm
  };
  


使用event从vm._events中取出事件监听器回调函数列表,并将其赋值给变量cbs,如果cbs存在,依次调用每一个监听器回调并将其所有参数传给监听器回调。
toArray的作用是将类似于数组的数据换成真正的数组,它的第二个参数是起始位置,也就是说,args是一个数组,里面包含除第一个参数之外的所有参数。

o n 和 on和 onemit可以结合起来使用,大部分用于父子组件传值,但是这里有一个问题

1、究竟是由子组件内部主动传数据给父组件,由父组件监听接收(由子组件中操作决定什么时候传值)

2、还是通过父组件决定子组件什么时候传值给父组件,然后再监听接收 (由父组件中操作决定什么时候传值)

两种情况都有

m e i t 事 件 触 发 , 通 过 子 组 件 内 部 的 事 件 触 发 自 定 义 事 件 meit事件触发,通过子组件内部的事件触发自定义事件 meit,emit

m e i t 事 件 触 发 , 可 以 通 过 父 组 件 操 作 子 组 件 ( r e f ) 的 事 件 来 触 发 自 定 义 事 件 meit事件触发, 可以通过父组件操作子组件 (ref)的事件来触发 自定义事件 meit(ref)emit

第一种情况

父组件 所以msg打印出来就是蜡笔小柯南


子组件

第二种情况

父组件 通过ref操作子组件触发事件

子组件


将两者情况对比,区别就在于$emit 自定义事件的触发是有父组件还是子组件去触发

第一种,是在子组件中定义一个click点击事件来触发自定义事件$emit,然后在父组件监听

第二种,是在父组件中第一一个click点击事件,在组件中通过refs获取实例方法来直接触发事件,然后在父组件中监听

用法:

移除自定义事件监听器。

如果没有提供参数,则移除所有的事件监听器;

如果只提供了事件,则移除该事件所有的监听器;

如果同时提供了事件与回调,则只移除这个回调的监听器。

Vue.prototype.$off = function(event,fn){
    const vm = this;
    //1-1、没有提供参数的情况,此时需要移除所有事件的监听器
    if(!arguments.length){
        // 当arguments.lengtha为0时,说明没有任何参数,这时需要移除所有事件监听器
        // 因此重置了vm._events属性,vm._events存储所有事件,所以将vm._events重置
        // 为初始状态就等同于将所有事件都移除了。
        vm._events = Object.create(null);
        return vm;
    }
    // 1-2、vm.$off的第一个参数支持数组,当event为数组的时候,只需要将数组遍历一遍,然后
    // 数组中的每一项依次调用vm.$off;
    if(Array.isArray(event)){
        for(let i = 0 ; i<event.length; i++){
            this.$off(event[i],fn)
        }
        return vm;
    }
    // 2、只提供了事件名,则移除该事件所有的监听器,只需要将vm._events中的event重置为空就行
    const cbs = vm._events[event];
    if(!cbs){
        return vm;
    }
    // 这里做了一个安全检测,如果这个事件没有被监听,vm._events内找不到任何监听器,直接退出
    // 程序,然后判断是否只有一个参数,如果是,将事件名在vm._events中所有事件都移除,只需要
    // 将vm._events上以该事件为属性的值设置为null即可
    if(arguments.length === 1){
        vm._events[event] = null;
        return vm;
    }
    // 3、如果同时提供了事件与回调,那么只移除这个回调的监听器,将使用参数中提供的事件名从
    // vm._events取出事件列表,然后从列表中找到与参数中提供的回调函数相同的那个函数,并
    // 将它从列表中移除
    if(fn){
        // 先判断是否有fn参数,有则说明用户同时提供了event和fn两个参数,然后从vm._events
        // 中取出事件监听器列表并遍历它,如果列表中的某一项与fn相同,或者某一项的fn属性与fn相同,
        // 使用splice方法将它从列表中移除,当循环结束后,列表中所有与用户参数中提供的fn相同的监听器
        // 都会被移除
        const cbd = vm._events[event];
        let cb;
        let i = cbs.length;
        while(i--){
            // 这里有一个细节要注意,在代码中遍历列表是从后向前循环,这样在列表中移除当前
            // 位置的监听器,不会影响列表中未遍历到的监听器位置,如果是从前向后遍历,那么当从
            // 列表中移除一个监听器时,后面的监听器会自动向前移动一个位置,会导致下一轮循环
            // 时跳过一个元素。
            cb = cbs[i];
            if(cb === fn || cb.fn === fn){
                cbs.splice(i,1);
                break;
            }
        }
    }
    return vm;
}

Vue.prototype.$once = function (event, fn) {
      var vm = this;
      function on () {
        vm.$off(event, on);
        fn.apply(vm, arguments);
      }
      on.fn = fn;
      vm.$on(event, on);
      return vm
    };
    

在vm. o n c e 中 调 用 v m . once中调用vm. oncevm.on来实现监听自定义事件的功能,当自定义事件触发后会执行拦截器,将监听器从事件列表中移除。

在vm. o n c e 使 用 v m . once使用vm. once使vm.on来监听事件,首先,将函数on注册到事件中,当自定义事件被触发时,会执行函数on(在这个函数中,会用时vm.$off将自定义事件移除),然后

手动执行函数,并将参数arguments传递给函数fn,这样就可以实现vm.$once的功能。

但是要注意on.fn = fn这行代码,前面介绍vm.$off时提到,在移除监听器时,需要将用户提供的监听器函数与列表中的监听器函数进行对比,相同部分会被移除,这导致当我们使用拦截器代替监听器注入到事件列表中时,

拦截器和用户提供的函数时不相同的,此时用户使用vm.$off来移除事件监听器,移除操作失败。

这个问题的解决方案是将用户提供的原始监听器保存到拦截器的fn属性中,当vm.$off方法遍历事件监听器列表时,同时会检查监听器和监听器的fn属性是否与用户提供的监听器函数相同,只要有一个相同,就说明需要被移除

的监听器找到了,将被找到的拦截器从监听器列表中移除

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值