这张图中有两个视图块A和B,它们分别又有两个子视图。这时候,如果子视图A1想要发出一个业务事件,使得B1和B2能够得到通知,过程就会是:
- 沿着父作用域一路往上到达双方共同的祖先作用域
- 从祖先作用域一级一级往下进行广播,直到到达需要的地方
简化一下:
对于这种事件的传播方式,举个例子,1班班长想要给2班班长发个通知,他的通知方向是一级一级向上汇报,直到双方共同的老师,然后再沿着路线向下去通知。 - 从作用域往上发送事件,使用scope.$emit
$scope.$emit("someEvent", {});
- 从作用域往下发送事件,使用scope.$broadcast
$scope.$broadcast("someEvent", {});
这两个方法的第二个参数是要随事件带出的数据。
注意,这两种方式传播事件,事件的发送方自己也会收到一份。
事件的接收与阻止:
无论是$emit还是$broadcast发送的事件,都可以被接收,接收这两种事件的方式是一样的:
$scope.$on("someEvent", function(e) {
// 这里从e上可以取到发送过来的数据
});
注意,事件被接收了,并不代表它就中止了,它仍然会沿着原来的方向继续传播,也就是:
- $emit的事件将继续向上传播
- $broadcast的事件将继续向下传播
有时候,我们希望某一级收到事件之后,就让它停下来,不再传播,可以把事件中止。这时候,两种事件的区别就体现出来了,只有$emit发出的事件是可以被中止的,$broadcast发出的不可以。
如果想要阻止$emit事件的继续传播,可以调用事件对象的stopPropagation()方法。
$scope.$on("someEvent", function(e) {
e.stopPropagation();
});
首先,调用事件对象的preventDefault()方法,然后,在收取这个事件对象的时候,判断它的defaultPrevented属性,如果为true,就忽略此事件。这个过程比较麻烦,其实我们一般是不需要管的,只要不监听对应的事件就可以了。在实际使用过程中,也应当尽量少使用事件的广播,尤其是从较高的层级进行广播。
上级作用域
$scope.$on("someEvent", function(e) {
e.preventDefault();
});
下级作用域
$scope.$on("someEvent", function(e) {
if (e.defaultPrevented) {
return;
}
});
事件总线
在Angular中,不同层级作用域之间的数据通信有多种方式,可以通过原型继承的一些特征,也可以收发事件,还可以使用服务来构造单例对象进行通信。
前面提到的这个军队的例子,有些时候沟通效率比较低,特别是层级多的时候。想象一下,刚才这个只有三层,如果更复杂,一个排长的消息都一定要报告到军长那边再下发到其他基层主官,必定贻误军情,更何况有很多下级根本不需要知道这个消息。
那怎么办呢,难道是直接打电话沟通吗?这个效率高是高,就是容易乱,这也就相当于界面块之间的直接通过id调用。
Angular的作用域树类似于传统的组织架构树,一个大型企业,一般都会有若干层级,近年来有很多管理的方法论,比如说组织架构的扁平化。
我们能不能这样:搞一个专门负责通讯的机构,大家的消息都发给它,然后由它发给相关人员,其他人员在理念上都是平级关系。
这就是一个很典型的订阅发布模式,接收方在这里订阅消息,发布方在这里发布消息。这个过程可以用这样的图形来表示:
app.factory("EventBus", function() {
var eventMap = {};
var EventBus = {
on : function(eventType, handler) {
//multiple event listener
if (!eventMap[eventType]) {
eventMap[eventType] = [];
}
eventMap[eventType].push(handler);
},
off : function(eventType, handler) {
for (var i = 0; i < eventMap[eventType].length; i++) {
if (eventMap[eventType][i] === handler) {
eventMap[eventType].splice(i, 1);
break;
}
}
},
fire : function(event) {
var eventType = event.type;
if (eventMap && eventMap[eventType]) {
for (var i = 0; i < eventMap[eventType].length; i++) {
eventMap[eventType][i](event);
}
}
}
};
return EventBus;
});
事件订阅代码:
EventBus.on("someEvent", function(event) {
// 这里处理事件
var c = event.data.a + event.data.b;
});
事件发布代码:
EventBus.fire({
type: "someEvent",
data: {
aaa: 1,
bbb: 2
}
});
注意,如果在复杂的应用中使用事件总线,需要慎重规划事件名,推荐使用业务路径,比如:“portal.menu.selectedMenuChange”,以避免事件冲突。