XIANFESchool / FE-problem-collection

前端问题收集和知识经验总结

Home Page:https://github.com/ShuyunXIANFESchool/FE-problem-collection/issues

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ng中的事件订阅与发布

fnjoe opened this issue · comments

commented

ng中的事件机制

Scopes can propagate(传送) events in similar fashion(类似的方式) to DOM events. The event can be broadcasted(向下广播) to the scope children or emitted(向上发布) to scope parents.

ng中的作用域拥有发送事件,携带信息的能力,类似于DOM的事件类型,ng中的Scope拥有$emit向上发布到$rootScope,$broadcast向下广播到所有的子节点,当我们想在不同作用域之间做数据交互的时候,不免会考虑到使用事件机制携带数据,在指定的作用域上使用$on接受即可。

但我们在实际中使用事件机制来携带数据时,可能需要接受事件来获取数据的作用域只有一两处,而ng的事件模型却帮我们遍历了所有的子作用域或者父作用域,换句话说,我们使用了一种大范围的广播携带数据,却只在某一处有价值的使用了数据,这点,就造成了性能上的浪费。

参考《精通AngularJS》

如果不是全局的,异步的,关于状态变迁的通知,还是应该谨慎使用,通常可以依赖双向数据绑定,便捷的解决问题。

通过事件携带数据的目的

  1. 事件的注册与触发拥有明显的前后条件逻辑
  2. 在不同的作用域之间传递信息。

考虑到ng中服务全局可见,并且能够被依赖注入,所以,我们可以考虑利用服务来指定目标来传递数据,避免性能上的浪费。
服务传递数据很容易实现,但是如何给服务增加注册与触发的先后条件逻辑呢?我们先来看看事件的先后条件逻辑是如何形成的。

JS实现事件机制

事件携带数据很形象的模拟了一组交互过程,通过事件的注册,事件的触发,来完成所需要的逻辑。基于此,我们可以在js中来模拟事件传播机制。
事件的过程主要分为三个步骤:

  1. 基于一个事件名,注册事件处理函数,在相应的事件发生时,该函数便会执行。

  2. 在需要触发事件的地方,调用该事件的所有已注册函数。

  3. 最重要的一点,我们如何能够将同一个事件的注册函数与事件发生对应起来,达到触发的效果呢。基于事件名与事件处理函数一对多的映射关系,我们使用js中的对象来储存这种映射关系,我们称之为事件队列。

    综上,我们的事件对象应该至少拥有

    1. 注册事件处理函数的方法。
    2. 触发事件的方法。
    3. 用于储存映射关系的事件队列。
                // 事件对象的构造函数,每个实例拥有一个subscribers 来储存自己的事件队列。
        var EventBus = function() {
            this.subscribers = [];
        };
               // 原型中拥有事件的注册方法和触发方法,为了方便管理,我们扩展除了删除事件队列的方法
        EventBus.prototype = {
            constructor: EventBus,
            // 注册方法,返回接收event标识符
            sub: function(evt, fn) {
                this.subscribers[evt] ? this.subscribers[evt].push(fn) : (this.subscribers[evt] = []) && this.subscribers[evt].push(fn);
                return '{"evt":"' + evt + '","fn":"' + (this.subscribers[evt].length - 1) + '"}';
            },
            // 触发方法,成功后返回自身
            pub: function() {
                var evt = arguments[0];
                var args = [].slice.call(arguments, 1);
                if (this.subscribers[evt]) {
                    for (var i in this.subscribers[evt]) {
                        if (angular.isFunction(this.subscribers[evt][i])) {
                            this.subscribers[evt][i].apply(null, args);
                        }
                    };
                    return this;
                }
                return false;
            },
            // 解除注册,需传入接收event标识符
            unsub: function(subId) {
                try {
                    var id = angular.fromJson(subId);
                    this.subscribers[id.evt][id.fn] = null;
                    delete this.subscribers[id.evt][id.fn];
                } catch (err) {
                    console.log(err);
                }
            }
        };

使用服务注入事件机制

通过以上逻辑,我们就可以实现自定义的事件机制。

那么,如何在不同作用域中使用我们的事件机制来携带数据呢,很简单,将EventBus 改写成可以被全局注入的服务即可。

巧妙的是,我们利用服务保存的不是需要传递的数据,而是事件队列,数据通过队列中方法的参数来传递。这样我们的事件机制与ng中的服务便完美的结合在一起了。

由于采用构造函数的写法,我们可以很方便的使用service方法来实现。

这样,我们自己的事件机制就可以使用了,由于我们的事件服务需要手动注入到需要使用的作用域内,不需要作用域的遍历,这样,相比使用scope的事件传播,具有简单明确的优点,精简了我们的逻辑。

目前来看,EventBus 可以解决 AngularJS 中不同控制器或指令之间的通讯问题,但是当你的事件注册到一定数量的时候,就变得很难维护,因为任何人都可以在控制器中发布事件和注册事件,所以我们需要将新事件的定义统一放到一个地方进行管理 例如, 像js-signals 中的处理方式:

新事件定义

 //store local reference for brevity
  var Signal = signals.Signal;

  //custom object that dispatch signals
  var myObject = {
    started : new Signal(), //past tense is the recommended signal naming convention
    stopped : new Signal()
  };

事件的订阅与发布

 function onStarted(param1, param2){
    alert(param1 + param2);
  }
  myObject.started.add(onStarted); //add listener
  myObject.started.dispatch('foo', 'bar'); //dispatch signal passing custom parameters
  myObject.started.remove(onStarted); //remove a single listener

基于 js-signals,可以将新事件的定义,放入 provider 中,在 config 阶段声明,这样等于在AngularJS应用中,有一块区域对自定义事件进行统一管理,可以对相应的事件进行启用和禁用或者利用装饰器加入日志功能,方便日后的跟踪和处理。

将 eventBus 改造如下:

(function() {
    angular.module('app').provider('eventBus', eventBus);

    var events = [];
        // provider 方法
    this.setEvent = function(eventName) {
        events.push(eventName);
    };

        // service 方法
    this.$get = eventBus;

    function eventBus() {

        var EventBus = function() {
            this.subscribers = [];
        };

        EventBus.prototype = {
            constructor: EventBus,
            // 订阅方法,返回订阅event标识符
            sub: function(fn) {
                this.subscribers.push(fn);
                return this.subscribers.length - 1;
            },
            // 发布方法,成功后返回自身
            pub: function() {
                var args = [].slice.call(arguments);
                for (var i in this.subscribers) {
                    if (angular.isFunction(this.subscribers[i])) {
                        this.subscribers[i].apply(null, args);
                    }
                };
                return this;
            },
            // 解除订阅,需传入订阅event标识符
            unsub: function(subId) {
                try {
                    this.subscribers.splice(subId, 1);
                } catch (err) {
                    console.log(err);
                }
            }
        };

        var eventBuses = {};

        events.forEach(function(name) {
            eventBuses[name] = new EventBus();
        });

        return eventBuses;
    }
})();

一点修改,订阅函数返回注销函数

(function() {
    angular.module('app').provider('eventBus', eventBus);

    var events = [];
    // provider 方法
    this.setEvent = function(eventName) {
        events.push(eventName);
    };

    // service 方法
    this.$get = eventBus;

    function eventBus() {

        var EventBus = function() {
            this.subscribers = [];
        };

        EventBus.prototype = {
            constructor: EventBus,
            // 订阅方法,返回注销函数
            sub: function(fn) {
                var _this = this;
                _this.subscribers.push(fn);
                return function() {
                    var index = _this.subscribers.indexOf(fn);
                    if (index >= 0) {
                        _this.subscribers[index] = null;
                    }
                }
            },
            // 发布方法,成功后返回自身
            pub: function() {
                var args = [].slice.call(arguments);
                var i = 0;
                while (i < this.subscribers.length) {
                    if (this.subscribers[i] === null) {
                        this.subscribers.splice(i, 1);
                    } else {
                        this.subscribers[i].apply(null, args);
                        i++;
                    }
                }
                return this;
            }
        };

        var eventBuses = {};

        events.forEach(function(name) {
            eventBuses[name] = new EventBus();
        });

        return eventBuses;
    }
})();