公司項目開發用的是angularjs,關於事件通訊一直用的是EventBus,直到上周寫一個小組件懶得引用EventBus時,想到用angularjs自帶的事件通信時,結果很尷尬的忘記原生方法單詞怎么寫了....
可能現在記錄這個真的算很晚了,包括對於顯得有些老舊的angularjs,但我們學習的畢竟是思想,而非框架,所以還是獨立一篇文章來聊聊angularjs中的事件通信$on,$emit與$broadcast。
一、為什么要用事件通信?
為什么要用事件通信?肯定要用啊,不用又解決不了問題,只能用事件通信維持生活這樣子;在聊這個之前,先簡單說說常見的跨作用域通信的幾種場景。
問題情景一:父controller傳子controller
我們在用angularjs日常開發中,跨作用域傳值十分常見;比如父級作用域有一個屬性,想跨作用域傳遞給子級,初學者可能習慣把這個屬性綁在父級scope上,通過作用域繼承讓子級作用域可以直接使用。
<div ng-controller="parentCtrl"> <span>我是父作用域</span> <div ng-controller="childCtrl">我是子作用域:{{name}}</div> </div>
let parentCtrl = function ($scope) { //父作用域定義name屬性 $scope.name = "聽風是風"; }; let childCtrl = function ($scope) { //子作用域通過繼承得到name屬性 }; angular.module('myApp', []) .controller("parentCtrl", ['$scope', parentCtrl]) .controller("childCtrl", ['$scope', childCtrl])
這是一種解決方法,但是將所有屬性方法綁在scope上並不是很好的做法,這會讓父子作用域的屬性顯得特別混亂。(先不談controller as vm);
問題情景二:父作用域傳值給子組件
這樣的情況也十分常見,父作用域有個值在子組件中也需要使用,友好一點,我們通過bindings來解決這個問題:
<div ng-controller="parentCtrl as vm"> <ting-feng echo="vm.name"></ting-feng> </div>
let parentCtrl = function () { let vm = this; vm.name = '聽風是風'; }; angular.module('myApp', []) .controller('parentCtrl', parentCtrl) .component('tingFeng', { template: '<div>我是子組件,我的名字是:{{self.echo}}</div>', controllerAs: 'self', bindings: { echo: '<' }, controller: function () {} });
可以看到,這次沒利用scope繼承,使用了組件的屬性也順利完成了傳值,問題不大。
問題情景三:異步跨作用域傳值
有時候,我們在父作用域拿值是一個異步操作,我們不知道什么時候才能拿到值,這也決定了我們不知道什么時候才可以把這個值傳給子作用域。
舉個實際的例子,現在需求要做一個loading組件,頁面初始化時顯示loading,當請求完成,由父作用域通知loading組件隱藏loading效果。
很明顯,此時通過bindings依舊能解決問題,難度不大。
上面三種情景均為父作用域傳值給子作用域,我們現在反過來,通通由子作用域傳父作用域,怎么解決?有什么一種方法能簡單的的處理這三種情況呢?當然有,事件通信。
二、什么是事件通信?
angularjs的事件通信分為監聽和派發兩個重要階段,有人(無中生有)對於監聽和派發理解總是很模糊,其實不難理解。
大家在為dom綁定事件時一定用過事件監聽addEventListener,比如:
document.getElementById("#div").addEventListener("click", function () { console.log('我被點擊啦'); });
在上述代碼中,我們為一個id為div的元素添加了一個事件監聽,它由click觸發;當用戶點擊這個元素,就會執行事件監聽的回調函數,也就是我們希望點擊后執行的操作。
angularjs的事件通信其實和這個有些類似,我們總是先去監聽一個東西,然后在滿足某個情況下再去觸發它,執行你想要的回調。
比如我在前面舉的loading組件的例子,首先loading默認顯示,並且監聽控制是否顯示的字段;當父作用域請求后台完畢,調用父傳子的方法;子組件響應監聽,執行回調拿到狀態值,並修改loading的顯示狀態。我們拆分步驟:
1.loading子組件默認顯示,監聽isShow字段。
2.父作用域請求完畢,對應isShow字段派發事件。
3.子組件作響應監聽,修改isShow,隱藏loading;
記住,永遠都是先監聽,后派發,這就像我們永遠都是先聲明函數,后調用它;如果你調用一個都為聲明的函數,又怎么起作用域呢?
angularjs提供了用於監聽的方法,還分別提供了父傳子,子傳父的方法,所以很好解決前面提到的子作用域希望傳值給父作用域的情況。
三、angularjs事件通信$on,$emit,$broadcast方法
1.$broadcast方法
父作用域傳給子作用域使用的方法,常見寫法:
$scope.$broadcast(eventName,data);
eventName:父傳子派發事件名稱,與子作用域中的監聽名保持一致,必寫項。
data:需要傳遞的數據如果沒有,可以不寫。
注意:此方法一般寫在某個觸發條件中,比如請求完成后派發;被點擊事件包裹,點擊時派發,不能直接寫;原因前提說過了,事件通信永遠滿足一點先監聽后派發,你得保證監聽在派發前初始化完成,這個涉及到了angularjs聲明周期的問題,這里先不細談。
2.$emit方法
子作用域傳給父作用域使用的方法,常見寫法:
$scope.$emit(eventName,data);
eventName:子傳父派發事件名稱,與父作用域中的監聽名保持一致,必寫項。
data:需要傳遞的數據,如果沒有,可以不寫。
3.$on方法
監聽方法,與$emit,$broadcast配合使用,比如在父作用域派發給子,父作用域中使用$broadcast方法,那么對應的子作用域中就是用$on方法進行監聽。常見寫法:
$scope.$on('eventName', function (event, data) { // do something... });
eventName:監聽名,與子傳父,或父傳子的事件派發名相同,必寫。
event:有多個方法屬性,這個對象使用不多。
currentScope:響應派發的當前作用域
targetScope:派發事件的原始作用域
name:事件名
defaultPrevented:默認為false
preventDefault:調用此方法defaultPrevented會變為true。
data:派發過來的數據,如果沒有,可以不寫。
四、一個例子
這里模擬一個前面提到的loading加載的例子,假設3秒后數據請求完成,則派發事件,通知子組件關閉loading顯示。這里我們沒有傳遞任何值給子組件,只是單純的通知子組件要去做什么。
<div ng-controller="parentCtrl as self"> <on-loading></on-loading> </div>
let parentCtrl = ($scope, $timeout) => { //三秒后派發事件,通知loading組件關閉顯示 $timeout(() => { $scope.$broadcast('loadingEnd'); }, 3000); }; angular.module('myApp', []) .controller("parentCtrl", ['$scope', '$timeout', parentCtrl]) .component('onLoading', { template: '<b>{{vm.num}}---{{vm.loading}}</b>', controllerAs: 'vm', controller: function ($scope, $interval) { let vm = this; //初始化loading狀態,默認顯示 vm.loading = 'loading進行中'; vm.num = 3; $interval(() => { vm.num ? vm.num-- : null; }, 1000) //監聽,用於響應事件派發 $scope.$on('loadingEnd', () => { vm.loading = 'loading已關閉'; }); } });
大致效果如下:
我在前面說,公司對於angularjs事件通信使用的是EventBus,為什么呢?主要是因為angularjs並未提供兄弟之間通信的方法,而使用EventBus不用考慮這點,不管什么作用域,都能很便捷的通信。
我后面會專門花一篇文章介紹EventBus,加上最近和同學打算一起開個一個個人的小程序,可能未來時間也比較緊張,反正一定會更新,也是為了自己。
那么本文就寫到這里了。
2019.9.17:解決父子組件異步傳值,不用事件通信,只用ng-if也能搞定,有興趣閱讀 angularjs ng-if妙用,ng-if解決父子組件異步傳值 這篇文章。