不一樣controller做用域之間通訊的方式

 

最近在作d3js + angularjs項目中,常常遇到d3組件與angularjs模塊間通訊的問題,以及angularjs多個做用域之間互相通訊的問題。關於angularjs的做用域概念及其繼承模式,這裏有一篇我以爲不錯的文章,不瞭解的朋友能夠先去看看。 javascript

本文主要談angularjs多個做用域之間如何互相通訊。咱們常常遇到這樣的需求:A做用域這裏有一個值改變了,如何通知做用域B相應值去改變。爲此我一直在尋找最佳實踐,尤爲是對於做用域不少,包含關係複雜的狀況。從簡單到複雜,方法總結以下: html

1.$rootscope

你們都知道$scope是html和單個controller之間的橋樑,數據綁定就靠他了。而$rootscope能夠被認爲是全局$scope, 在各個controller裏面均可以顯示,也均可以修改。 前端

下例展現瞭如何在$rootscope上建立一個對象和使用其中的數據: java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
angular . module ( 'myApp' , [ ] )
. run ( function ( $ rootScope ) {
     $ rootScope . test = new Date ( ) ;
} )
. controller ( 'myCtrl' , function ( $ scope , $ rootScope ) {
   $ scope . change = function ( ) {
         $ scope . test = new Date ( ) ;
     } ;
 
     $ scope . getOrig = function ( ) {
         return $ rootScope . test ;
     } ;
} )
. controller ( 'myCtrl2' , function ( $ scope , $ rootScope ) {
     $ scope . change = function ( ) {
         $ scope . test = new Date ( ) ;
     } ;
 
     $ scope . changeRs = function ( ) {
         $ rootScope . test = new Date ( ) ;
     } ;
 
     $ scope . getOrig = function ( ) {
         return $ rootScope . test ;
     } ;
} ) ;

優勢: react

  • 簡單易懂

缺點: jquery

  • 全局變量污染

適用範圍: git

頻繁的使用$rootscope會形成全局變量污染,但我也反對部分代碼潔癖者徹底拒絕$rooScope的做風。一些特別頻繁調用的方法,徹底能夠該放在$rootscope裏。對於少許一旦登陸就會處處顯示,而且不太容易變化的變量,徹底可使用$rootScope來保存。例如系統的登陸用戶名,通常登陸之後就基本不會變,還要在各個做用域中顯示。那麼對於少許此類變量,爲什麼不用$rootScope來儲存呢?除此之外,$rootScope還有一個特別有用的特性,那就是它處於全部scope的最頂層,在事件傳播中有妙用,在一個通用的訂閱/發佈模式的angularjs通訊模塊中,幾乎少不了使用$rootScope。這一點在後文中會有詳細描述。 程序員

2.做用域繼承

做用域嵌套帶來的父子做用域的繼承關係也能夠算是一種父子做用域之間的通訊方式。 angularjs

1
2
3
4
5
6
7
< div ng - controller = "Parent" >
   < div ng - controller = "Child" >
     < div ng - controller = "ChildOfChild" >
       < button ng - click = "someParentFunctionInScope()" > Do < / button >
     < / div >
   < / div >
< / div >

優勢: github

  • 對於從祖先到子孫的數據傳遞效果很好

缺點:

  • 從子孫到祖先的數據傳遞效果很差,子 Scope 的屬性會隱藏(覆蓋)了父 Scope 中的同名屬性,對子 Scope 屬性的更改並不更新父 Scope同名屬性的值。(這個行爲實際上不是 AngularJS 特有的,JavaScript 自己的原型鏈就是這樣工做的。)
  • 不能進行兄弟做用域的數據傳遞,除非用一個共同祖先,例如$rootScope
  • 調用祖先函數意味着祖先與子孫之間的緊密耦合,當程序複雜到必定程度時修改起來會致使牽一髮動全身的悲慘結局

適用範圍:

從上面的優缺點分析中咱們能夠看到做用域繼承方法有很大的侷限性。故而做用域繼承一般只用在簡單、小型的模塊中,例如directive指令的書寫中。

3.做用域繼承+$watch

爲了解決做用域繼承不能解決的從子孫到祖先的數據傳遞問題,能夠用$scope.$watch函數來監視數據變化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//父做用域監視子做用域
. controller ( "Parent" , function ( $ scope ) {
   $ scope . VM = { a : "a" , b : "b" } ;
   $ scope . $ watch ( "VM.a" , function ( newVal , oldVal ) {
     // react
   } ) ;
}
 
//子做用域監視父做用域
. controller ( "child" , function ( $ scope ) {
     $ scope . $ parent . $ watch ( $ scope . VM . a , function ( ) {
       //react
     } ) ;
}

angularjs的指令書寫模式中,還有一種指定指令的scope的方式,本質上與此相通,諸如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ compileProvider . directive ( 'anrow' , function ( $ compile ) {
         return {
             require :      '^angrid' ,
             restrict :    'E' ,
             transclude : true ,
             scope :        {
                 anrowData :      "=anrowData" ,
                 selectedItems : "=selects" ,
                 searchFilter :    "=searchFilter"
             } ,
             template : '' ,         
             replace :      true ,
                         link : function ( scope , element , attrs , angridCtrl ) {
                         }
                 }
 
} ) ;

優勢:

  • 適合用在非controller生成的子做用域中,例如ng-repeat生成的大量自做用域中

缺點:

  • 對於one-time events(一次性事件)沒什麼效果
  • $watch函數相似eval函數,寫出來的代碼不宜讀

適用範圍:

$watch函數功能很強大,配合$scope.$parent和本來的繼承關係能夠實現父子做用域的各類數據傳遞。偶爾還會須要使用$scope.$eval方法來將字符串變爲對象。這種方法用在總體架構的controller中或許會致使代碼難以讀懂,可是用在一些j較爲獨立的angularjs指令或插件中確是極好的。例如我寫的angularjs 表格插件angrid就是用這個方法寫的。表格中包含大量ng-repeat生成的自做用域,幾乎都是用這種方式來實現。

4.消息機制

異步回調響應式通訊—事件機制是javascript解決模塊通訊的最經常使用手段。在angularjs中此方法表現爲由$scope下定義的三個函數$broadcast, $emit, $on組成的事件隧道通訊機制。這裏我援引 破狼的博客  Angularjs Controller 間通訊機制 來簡單地說明這個方法怎麼用:

 Angularjs爲在scope中爲咱們提供了冒泡和隧道機制,$broadcast會把事件廣播給全部子controller,而$emit則會將事件冒泡傳遞給父controller,$on則是angularjs的事件註冊函數,有了這一些咱們就能很快的以angularjs的方式去解決angularjs controller之間的通訊,代碼以下:

1
2
3
4
5
6
7
8
< div ng - app = "app" ng - controller = "parentCtr" >
     < div ng - controller = "childCtr1" > name :
         < input ng - model = "name" type = "text" ng - change = "change(name);" / >
     < / div >
     < div ng - controller = "childCtr2" > Ctr1 name :
         < input ng - model = "ctr1Name" / >
     < / div >
< / div >

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
angular . module ( "app" , [ ] ) . controller ( "parentCtr" ,
function ( $ scope ) {
     $ scope . $ on ( "Ctr1NameChange" ,
 
     function ( event , msg ) {
         console . log ( "parent" , msg ) ;
         $ scope . $ broadcast ( "Ctr1NameChangeFromParrent" , msg ) ;
     } ) ;
} ) . controller ( "childCtr1" , function ( $ scope ) {
     $ scope . change = function ( name ) {
         console . log ( "childCtr1" , name ) ;
         $ scope . $ emit ( "Ctr1NameChange" , name ) ;
     } ;
} ) . controller ( "childCtr2" , function ( $ scope ) {
     $ scope . $ on ( "Ctr1NameChangeFromParrent" ,
 
     function ( event , msg ) {
         console . log ( "childCtr2" , msg ) ;
         $ scope . ctr1Name = msg ;
     } ) ;
} ) ;

 

這裏childCtr1的name改變會以冒泡傳遞給父controller,而父controller會對事件包裝在廣播給全部子controller,而childCtr2則註冊了change事件,並改變本身。注意父controller在廣播時候必定要改變事件name。

jsfiddle連接:http://jsfiddle.net/whitewolf/5JBA7/15/

優勢:

  • 對一次性事件的效果很好
  • 事件機制能夠有效下降controller之間的耦合度

缺點:

  • 因爲DOM樹事件響應機制等緣由,angularjs裏的事件機制也是採起冒泡+廣播的方式,不能像C語言中那樣定義事件觸發和響應槽這樣的直接響應關係。這個因素直接致使以下兩個問題:
  • 不能進行兄弟做用域的數據傳遞,除非用一個共同祖先,例如$rootScope
  • 相比$emit冒泡方法,$broadcast廣播方法要消耗更多的資源,由於廣播事件會深刻到該做用域的全部子孫做用域,跟單路徑冒泡的$emit消耗的資源徹底不是數量級。故而對於包含成數千子做用域又要追求較高性能的狀況,可能須要考慮一下是否棄用$broadcast方法。這裏有一個對比測試,$rootScope.emit() vs $rootScope.$broadcast, performance tests (http://jsperf.com/rootscope-emit-vs-rootscope-broadcast)。

適用範圍:

事件隧道機制能夠解決絕大部分事件通訊問題,也是這裏很是推薦的方式。

不過,當模塊複雜到必定程度,可能就要援引一些設計模式方面的知識才能解決問題,而事件隧道機制、$rootScope、scope繼承+$watch方式 都是成爲了實現設計模式的基本手段。

5.專用service

前文已經提到,能夠專門構建service來處理做用域間的通訊問題。若是controller之間有較強依賴,例如都會操做同一個數據集,那麼建立一個專門的service模塊來處理此類事務,比直接用事件隧道機制在邏輯上更清晰。一個最簡單的服務模塊的例子以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var myApp = angular . module ( 'myApp' , [ ] ) ;
 
myApp . factory ( 'Data' , function ( ) {
   return { message : "I'm data from a service" } ;
} ) ;
 
function FirstCtrl ( $ scope , Data ) {
   $ scope . data = Data ;
}
 
function SecondCtrl ( $ scope , Data ) {
   $ scope . data = Data ;
}

然而在實際應用中,僅僅把數據存取抽取出來是不足夠的,咱們還須要觸發機制,以保證ctrl1使數據變化後,ctrl2的數據也能跟着改變。對此有兩種辦法,其一是使用消息機制,其二是使用$watch方法來檢測數據變化。

使用$watch來監控數據變化的例子,這裏我找了一個比較典型的,由於比較長因此我只貼連接:http://jsbin.com/rifob/1/edit?html,js,output

下面這個例子則是使用事件通訊機制:代碼來自:http://jsfiddle.net/simpulton/XqDxG/。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var myModule = angular . module ( 'myModule' , [ ] ) ;
myModule . factory ( 'mySharedService' , function ( $ rootScope ) {
     var sharedService = { } ;
 
     sharedService . message = '' ;
 
     sharedService . prepForBroadcast = function ( msg ) {
         this . message = msg ;
         this . broadcastItem ( ) ;
     } ;
 
     sharedService . broadcastItem = function ( ) {
         $ rootScope . $ broadcast ( 'handleBroadcast' ) ;
     } ;
 
     return sharedService ;
} ) ;
 
function ControllerZero ( $ scope , sharedService ) {
     $ scope . handleClick = function ( msg ) {
         sharedService . prepForBroadcast ( msg ) ;
     } ;
 
     $ scope . $ on ( 'handleBroadcast' , function ( ) {
         $ scope . message = sharedService . message ;
     } ) ;         
}
 
function ControllerOne ( $ scope , sharedService ) {
     $ scope . $ on ( 'handleBroadcast' , function ( ) {
         $ scope . message = 'ONE: ' + sharedService . message ;
     } ) ;         
}
 
function ControllerTwo ( $ scope , sharedService ) {
     $ scope . $ on ( 'handleBroadcast' , function ( ) {
         $ scope . message = 'TWO: ' + sharedService . message ;
     } ) ;
}
 
ControllerZero . $ inject = [ '$scope' , 'mySharedService' ] ;         
 
ControllerOne . $ inject = [ '$scope' , 'mySharedService' ] ;
 
ControllerTwo . $ inject = [ '$scope' , 'mySharedService' ] ;

 

6.發佈/訂閱模式

顯然程序員並不以以上幾種方式此爲知足。前文講事件已經提到,當模塊複雜到必定程度,若是僅僅使用消息機制,同級做用域的交互都須要通過父做用域來傳遞消息,而且組件之間廣播消息意味着它們須要多少知道一些其它組件編碼的細節,這樣就限制了它們的模塊化和重用。這個時候就要引用一些設計模式的方法來解決問題。而實現他們的手段,就是以上提到的 $rootScope, scope繼承+$watch, 消息機制 和 自定義service。考慮咱們所要的需求,一方面要保證多個controller(模塊)的數據一致性,一方面還要保證controller(或模塊)的模塊化和重用性,那麼在這種場合使用觀察者模式或其變種就很是合適。

在我查閱的資料中有不少國外程序員推薦使用使用發佈/訂閱模式。發佈/訂閱模式是觀察者模式的一個變種,也是消息隊列模式的一類,是解決多模塊操做同一數據集時的一種經常使用方案。  訂閱發佈模式定義了一種一對多的依賴關係,讓多個訂閱者對象同時監聽某一個主題對象。這個主題對象在自身狀態變化時,會通知全部訂閱者對象,使它們可以自動更新本身的狀態。能夠有效地實現模塊間的解耦,提升可維護性。

有個國外程序員就此作了一個demo而且寫了篇博客介紹他的實現,如今他的這篇文章已經有了翻譯後的版本。這種方法從本質上說是同構構建service來處理做用域間的通訊問題,仍是用$rootScope來作頂級父做用域,而且作了事件的發佈和接收所有封裝了起來。你們有興趣的話能夠直接到angularjs-pubsub 上下載代碼,研究他是怎麼作的。

不過因爲這位程序員完成他的demo較早,我我的感受其中還有不少能夠改進的地方。以下例是一個較爲簡單的發佈/訂閱模式的實現:此方法經過$rootScope定義一個簡單的發佈/訂閱者模式,並經過消息機制來進行發佈和訂閱。其中儘可能避免了$broadcast的使用。此代碼來自:http://jsfiddle.net/brendanowen/ADukg/47/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var myApp = angular . module ( 'myApp' , [ ] ) ;
 
myApp . service ( 'messageService' , [ '$rootScope' , function ( $ rootScope ) {
 
     return {
         publish : function ( name , parameters ) {
             $ rootScope . $ emit ( name , parameters ) ;
         } ,
         subscribe : function ( name , listener ) {
             $ rootScope . $ on ( name , listener ) ;
         }
     } ;
} ] ) ;
 
myApp . controller ( 'MyCtrl' , [ '$scope' , 'messageService' , function ( $ scope , messageService ) {
     $ scope . showDialog = false ;
     $ scope . name = 'Superhero' ;
 
     $ scope . show = function ( ) {
         messageService . publish ( 'dialog' , { show : true } ) ;
     } ;
 
     messageService . subscribe ( 'dialog' , function ( event , parameters ) {
         $ scope . showDialog = parameters . show ;
     } ) ;
 
} ] ) ;
 
myApp . controller ( 'Dialog' , [ '$scope' , 'messageService' , function ( $ scope , messageService ) {
 
     $ scope . hide = function ( ) {
         messageService . publish ( 'dialog' , { show : false } ) ;
     } ;
} ] ) ;

此外還有不使用消息機制,純粹用自定義的消息隊列來實現的發佈/訂閱模式的代碼範例。注意其中使用了jquery。下面的代碼來自:https://gist.github.com/floatingmonkey/3384419

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
'use strict' ;
 
( function ( ) {
     var mod = angular . module ( "App.services" , [ ] ) ;
//register other services here...
     /* pubsub - based on https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js*/
     mod . factory ( 'pubsub' , function ( ) {
         var cache = { } ;
         return {
             publish : function ( topic , args ) {
                 cache [ topic ] && $ . each ( cache [ topic ] , function ( ) {
                     this . apply ( null , args || [ ] ) ;
                 } ) ;
             } ,
             subscribe : function ( topic , callback ) {
                 if ( ! cache [ topic ] ) {
                     cache [ topic ] = [ ] ;
                 }
                 cache [ topic ] . push ( callback ) ;
                 return [ topic , callback ] ;
             } ,
             unsubscribe : function ( handle ) {
                 var t = handle [ 0 ] ;
                 cache [ t ] && d . each ( cache [ t ] , function ( idx ) {
                     if ( this == handle [ 1 ] ) {
                         cache [ t ] . splice ( idx , 1 ) ;
                     }
                 } ) ;
             }
         }
     } ) ;
     return mod ;
} ) ( ) ;

最後是我在書寫這篇博客前,在查閱大量資料的基礎上,總結而成的一個angularjs的 發佈/訂閱 模式 服務模塊,目前下面的代碼正應用於個人項目。此代碼較好地解決了三個問題:

  • 結構清晰易讀,比$watch方式容易理解和使用
  • 不使用$broadcast,只用$emit來發布事件,效率較高。
  • 容許用戶控制cache,在controller了生命週期結束後自動解除$rootscope上的事件綁定,低功耗無污染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
define ( [
     '../../app'
] , function ( app ) {
     // 這是一個通用的 發佈訂閱模塊
     //參考:https://gist.github.com/turtlemonvh/10686980/038e8b023f32b98325363513bf2a7245470eaf80
     app . factory ( 'pubSubService' , [ '$rootScope' , function ( $ rootScope ) {
         // private notification messages
         var _DATA_UPDATED_ = '_DATA_UPDATED_' ;
         /*
         * @name : publish
         * @description: 消息發佈者,只用$emit冒泡進行消息發佈的低能耗無污染方法
         * @param : {string=}: msg, 要發佈的消息關鍵字,默認爲'_DATA_UPDATED_'指數據更新
         * @param : {object=}: data,隨消息一塊兒傳送的數據,默認爲空
         * @example :
         *         pubSubService.publish('config.itemAdded', {'id': getID()});
         *         更通常的形式是:
         *      pubSubService.publish();
         */
         var publish = function ( msg , data ) {
             msg = msg || _DATA_UPDATED_ ;
             data = data || { } ;
             $ rootScope . $ emit ( msg , data ) ;
         } ;
         /*
         * @name: subscribe
         * @description: 消息訂閱者
         * @param: {function}: 回調函數,在訂閱消息到來時執行
         * @param: {object=}: 控制器做用域,用以解綁定,默認爲空
         * @param: {string=}: 消息關鍵字,默認爲'_DATA_UPDATED_'指數據更新
         * @example:
         *         pubSubService.subscribe(function(event, data) {
         *        $scope.power = data.power;
         *            $scope.mass = data.mass;
         *        },  $scope, 'data_change');
         *        更通常的形式是:
         *        pubSubService.subscribe(function(){});
         */
         var subscribe = function ( func , scope , msg ) {
             if ( ! angular . isFunction ( func ) ) {
                 console . log ( "pubSubService.subscribe need a callback function" ) ;
                 return ;
             }
             msg = msg || _DATA_UPDATED_ ;
             var unbind = $ rootScope . $ on ( msg , func ) ;
             //可控的事件反綁定機制
             if ( scope ) {
                 scope . $ on ( '$destroy' , unbind ) ;
             }
         } ;
 
         // return the publicly accessible methods
         return {
             publish :          publish ,
             subscribe :        subscribe
         } ;
     } ] )
} ) ;

優勢:

  • 咱們能夠下降組件之間的耦合度,並將它們的之間通訊的細節封裝起來。在不影響主體功能的狀況下提升模塊的重用性。

缺點:

  • 缺點是增長了代碼複雜度,因此必定要寫足夠給力的接口說明文檔,不然時間久了本身都不知道發生了什麼事情。

適用範圍:

 

該方案推薦給內部含有大量做用域通訊,而且特別強調代碼重用性的場合使用。

發佈/訂閱模式已經能解決大部分複雜多模塊的通訊問題了。可是若是模塊不少,複雜度繼續上升,那麼會形成消息種類過多。這種時候有必要使用責任鏈模式來替換普通的發佈/訂閱模式。

設計模式的種類有不少,可是引入前端設計的並很少。不少人以爲設計模式難是由於不存在一個「完美」的模式,必須根據實際狀況來選用相應的模式,或者說,完美是不斷適應新狀況的能力。這種隨機應變的能力纔是真正考驗代碼設計者的問題。

—————————————————————————————————————

更多關於controller間通訊的討論請見:

http://stackoverflow.com/questions/11252780/whats-the-correct-way-to-communicate-between-controllers-in-angularjs/19498009#19498009

http://stackoverflow.com/questions/26751889/communication-between-controllers-in-angular

相關文章
相關標籤/搜索