ng中的事件订阅与发布
fnjoe opened this issue · comments
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》
如果不是全局的,异步的,关于状态变迁的通知,还是应该谨慎使用,通常可以依赖双向数据绑定,便捷的解决问题。
通过事件携带数据的目的
- 事件的注册与触发拥有明显的前后条件逻辑
- 在不同的作用域之间传递信息。
考虑到ng中服务全局可见,并且能够被依赖注入,所以,我们可以考虑利用服务来指定目标来传递数据,避免性能上的浪费。
服务传递数据很容易实现,但是如何给服务增加注册与触发的先后条件逻辑呢?我们先来看看事件的先后条件逻辑是如何形成的。
JS实现事件机制
事件携带数据很形象的模拟了一组交互过程,通过事件的注册,事件的触发,来完成所需要的逻辑。基于此,我们可以在js中来模拟事件传播机制。
事件的过程主要分为三个步骤:
-
基于一个事件名,注册事件处理函数,在相应的事件发生时,该函数便会执行。
-
在需要触发事件的地方,调用该事件的所有已注册函数。
-
最重要的一点,我们如何能够将同一个事件的注册函数与事件发生对应起来,达到触发的效果呢。基于事件名与事件处理函数一对多的映射关系,我们使用js中的对象来储存这种映射关系,我们称之为事件队列。
综上,我们的事件对象应该至少拥有
- 注册事件处理函数的方法。
- 触发事件的方法。
- 用于储存映射关系的事件队列。
// 事件对象的构造函数,每个实例拥有一个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;
}
})();