Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ng中的事件订阅与发布 #20

Open
fnjoe opened this issue Jul 26, 2016 · 2 comments
Open

ng中的事件订阅与发布 #20

fnjoe opened this issue Jul 26, 2016 · 2 comments

Comments

@fnjoe
Copy link

fnjoe commented Jul 26, 2016

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的事件传播,具有简单明确的优点,精简了我们的逻辑。

@hjzheng
Copy link
Member

hjzheng commented Aug 10, 2016

目前来看,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;
    }
})();

@hjzheng
Copy link
Member

hjzheng commented Sep 30, 2016

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

(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;
    }
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants