在angular scope中可以通过$on,$emit和$broadcast方法实现了自定义事件机制,实现数据共享,原理其实并不复杂,下面我会用例子来做分析,先看一段测试源码:
<div ng-controller="parentCtrl">
<div ng-controller="selfCtrl">
<a ng-click="click()">点击</a>
<div ng-controller="childCtrl"></div>
</div>
<div ng-controller="siblingCtrl"></div>
</div>
<script>
var app = angular.module('myApp',[]);
app.controller('selfCtrl', function ($scope) {
$scope.click = function () {
$scope.$broadcast('to-child', 'child');
$scope.$emit('to-parent', 'parent');
}
});
app.controller('parentCtrl', function ($scope) {
$scope.$on('to-parent', function (event, data) {
console.log('parentCtrl', data); //父级能得到值
});
});
app.controller('childCtrl', function ($scope) {
$scope.$on('to-child', function (event, data) {
console.log('childCtrl', data); //子级能得到值
});
});
app.controller('siblingCtrl', function ($scope) {
});
</script>
当按了点击之后,才会向上和向下派发事件,当我没按点击之前,parentCtrl上,和childCtrl上都有注册事件,我们从下往上看,
先看childCtrl scope:
id:表示作用域id。
listenerCount: 表示注册监听事件的总数包括子的监听事件,key是事件名,value是监听的总数(是个对象)。
listeners: 表示注册监听事件,key表示事件名,value是个回调函数(是个对象)。
在childCtrl上面只注册监听了一个to-child事件,从代码中和图中已经很明了了。
接下来看selfCtrl scope:
从代码中可以看出selfCtrl 中并没有注册监听事件,只有派发事件,还是按了点击之后才派发事件,所以它的listeners是空的,因为它的子作用域childCtrl上有注册to-child监听事件,所以$$listenerCount的value值是1。
再来看看selfCtrl的兄弟 siblingCtrl的scope:
代码中siblingCtrl controller里面都是空的,并且又没有子作用域,所以啥玩意没有。
最后咱们来看看最外层parentCtrl 的scope:
其实上面还有个rootScope,rootScope的id为1,这里我就不截图了,parentCtrl controller代码中我注册了个to-parent事件,listeners就这个事件,它的子selfCtrl上有个to-child事件,所以listenerCount 有两个值。所以我们可以得知,listeners是当前作用域的注册的监听事件,listenerCount是它的后代所有的注册的监听的总值。好,知道了这些,我们再来看它的源码就很简单了。
1.$on方法注册监听事件
直接上源码:
//这里参数是注册监听的事件名和回调fn
$on: function (name, listener) {
var namedListeners = this.$$listeners[name];
if (!namedListeners) {
this.$$listeners[name] = namedListeners = [];
}
//监听函数存到存值数组中
namedListeners.push(listener);
var current = this;
do {
//从子往父维护$$listenerCount的值
if (!current.$$listenerCount[name]) {
current.$$listenerCount[name] = 0;
}
current.$$listenerCount[name]++;
} while ((current = current.$parent));
var self = this;
//当执行了回调后,取消监听函数
return function () {
//判断存储数组中监听函数是否存在
var indexOfListener = namedListeners.indexOf(listener);
if (indexOfListener !== -1) {
//从存储中删除该监听函数
delete namedListeners[indexOfListener];
//删除完之后,从子往父维护$$listenerCount的值
decrementListenerCount(self, 1, name);
}
};
}
一开始listenerCount,listeners都是空对象,是在创建scope的时候创建的,刚注册的时候会先判下有没有注册这个事件,如果没有,就以key为事件名,value开始设置为一个空数组,然后把回调函数放进这个数组里,然后会判断listenerCount[key]有没有值,没有就设置listenerCount[key]=1;然后往上找父scope,在每个scope都设置listenerCount[key]=1;当然这个是刚开始的时候,之后listenerCount[key]++了,当收到了派发事件执行回调,取消监听函数,有了之前的测试代码分析,这里应该很清楚了,其实这里就是更新listenerCount,listeners这两个对象。
2.$emit向上冒泡传递事件
$emit 发出,放射的意思,就和火箭一样,肯定是向上传播了,通过scope不断向父scope传递消息,这里和js中的向上冒泡有点相似,也是从下往上传播,还是直接上源码吧:
//两参数为传播的事件名和值
$emit: function (name, args) {
var empty = [],
namedListeners,
scope = this,
//默认阻止冒泡是为false的
stopPropagation = false,
// 初始化event对象,也就传递给监听函数的event对象
event = {
name: name,
targetScope: scope,//这里是最初始的scope
stopPropagation: function () {
stopPropagation = true;
},
//阻止默认事件默认是不阻止的值为false,阻止之后为true
preventDefault: function () {
event.defaultPrevented = true;
},
defaultPrevented: false
},
//传递给监听函数的参数event对象和要传的值放进一个数组里面
listenerArgs = concat([event], arguments, 1),
i, length;
do {
// 循环处理作用域上的监听函数(从子到父逐个找注册的监听事件)
namedListeners = scope.$$listeners[name] || empty;
event.currentScope = scope;//当前的作用域
for (i = 0, length = namedListeners.length; i < length; i++) {
// 如果已注销监听器,事件取消
if (!namedListeners[i]) {
namedListeners.splice(i, 1);
i--;
length--;
continue;
}
try {
// 执行当前scope的回调
namedListeners[i].apply(null, listenerArgs);
} catch (e) {
$exceptionHandler(e);
}
}
//如果回调设置了stopPropagation为true,那么终止冒泡过程
if (stopPropagation) {
break;
}
// 向上遍历父作用域
scope = scope.$parent;
} while (scope);
// 处理完监听函数后,去除作用域引用
event.currentScope = null;
return event;
}
既然是冒泡,当然就有阻止冒泡的方法,angular在会传递给监听函数一个event对象,可以通过event.stopPropagation方法来做到这一点,$emit的原理不断循环处理父级作用域上的监听函数,源码中做了很多优化处理而已。
值得留意的有以下几个地方:
- 处理回调函数中空元素的逻辑。首先想想什么情况下才会出现这种情况呢?难道遍历中会发生事件的注销吗?答案是:是的,在回调函数就有可能把它自己给注销了。当只需要调用一次某个回调函数的时候,就会出现这种情况。
- 在以此遍历每个回调函数的时候,如果第一个回调函数改变了event或者是其它参数,后续的回调函数就能够发现并根据参数作出合适的处理。,比如第一个回调如果计算得到了一个值,就可以将该值放入到参数中供后续的回调函数使用。
- preventDefault这个flag并没有在遍历过程中被使用,这个flag可以在回调函数中使用,根据其值执行不同的业务逻辑。也可以在其它需要的地方使用,因为它也是返回的事件对象上的一个属性,这一点和stopPropagation不一样,后者并不是事件对象上的属性。
- 返回event对象之前,会清空其中定义的currentScope属性。因为该属性随着遍历会发生变化,因此将它暴露出去没有意义,在返回之前清空。
- 检测是否stopPropagation的逻辑发生在循环当前scope的所有回调之后。这样做能够保证当前scope上的所有回调都会被执行
3.$broadcast向下广播传递事件
和$emit一样需要向其他作用域传递消息,这里的传递的目标作用域不再是父scope,而是所有的子scope,避免深层次的循环嵌套,采用深度优先算法遍历作用域树,从而达到广播的效果,直接上源码:
//两参数为传播的事件名和值
$broadcast: function (name, args) {
var target = this,
//target是最初始的scope
current = target,
next = target,
// 初始化event对象,也就传递给监听函数的event对象
event = {
name: name,
targetScope: target,
//阻止默认事件默认是不阻止的值为false,阻止之后为true
preventDefault: function () {
event.defaultPrevented = true;
},
defaultPrevented: false
};
//因为是从父scope往子传播,如果父的$$listenerCount都没有值,那么子$$listeners肯定是没有值的,这里做了个优化!
if (!target.$$listenerCount[name]) return event;
//传递给监听函数的参数event对象和要传的值放进一个数组里面
var listenerArgs = concat([event], arguments, 1),
listeners, i, length;
//down while you can, then up and next sibling or up and next sibling until back at root
while ((current = next)) {
event.currentScope = current;
listeners = current.$$listeners[name] || [];
for (i = 0, length = listeners.length; i < length; i++) {
//和$emit一样,如果已注销监听器,事件取消
if (!listeners[i]) {
listeners.splice(i, 1);
i--;
length--;
continue;
}
try {
// 执行当前scope的回调
listeners[i].apply(null, listenerArgs);
} catch (e) {
$exceptionHandler(e);
}
}
// 和digest循环中一样实现了深度优先遍历,其中利用$$listenerCount做了性能优化(先找子,没有的话再找兄弟,再没有回到父)
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
}
event.currentScope = null;
return event;
}
}
原理和$emit没有什么区别,主要不同点在于, 没有stopPropagation,遍历的方式为深度优先遍历,这里listenerCount[name]值大于0才会遍历。这也算是性能上的优化吧。否则在没有注册回调函数的情况下,每次都遍历只会浪费性能。
到这里,scope事件执行机制就讲完了(如果有不对的地方,欢迎指出,谢谢!)下一篇文件我会讲下angular的执行流程和源码分析,好了,欢乐的时光总是过得特别快,又到时候和大家讲拜拜!!