在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的執行流程和源碼分析,好了,歡樂的時光總是過得特別快,又到時候和大家講拜拜!!