【问题标题】:What's the correct way to communicate between controllers in AngularJS?AngularJS中控制器之间通信的正确方法是什么?
【发布时间】:2012-06-30 10:53:06
【问题描述】:

控制器之间的正确通信方式是什么?

我目前正在使用涉及window 的可怕软糖:

function StockSubgroupCtrl($scope, $http) {
    $scope.subgroups = [];
    $scope.handleSubgroupsLoaded = function(data, status) {
        $scope.subgroups = data;
    }
    $scope.fetch = function(prod_grp) {
        $http.get('/api/stock/groups/' + prod_grp + '/subgroups/').success($scope.handleSubgroupsLoaded);
    }
    window.fetchStockSubgroups = $scope.fetch;
}

function StockGroupCtrl($scope, $http) {
    ...
    $scope.select = function(prod_grp) {
        $scope.selectedGroup = prod_grp;
        window.fetchStockSubgroups(prod_grp);
    }
}

【问题讨论】:

  • 完全没有实际意义,但在 Angular 中,您应该始终使用 $window 而不是原生的 JS 窗口对象。这样你就可以在你的测试中把它存根:)
  • 请参阅下面我关于此问题的答案中的评论。 $broadcast 不再比 $emit 贵。请参阅我在那里引用的 jsperf 链接。

标签: scope angularjs


【解决方案1】:

编辑:此答案中解决的问题已在 angular.js version 1.2.7 中得到解决。 $broadcast 现在可以避免在未注册的作用域上冒泡,并且运行速度与 $emit 一样快。

所以,现在您可以:

  • 使用$broadcast 来自$rootScope
  • 使用$on收听来自需要了解事件的本地$scope

原始答案如下

我强烈建议不要使用$rootScope.$broadcast + $scope.$on,而是使用$rootScope.$emit+ $rootScope.$on。前者可能会导致@numan 提出的严重性能问题。那是因为事件会在所有范围内冒泡。

但是,后者(使用$rootScope.$emit + $rootScope.$on)确实不会受此影响,因此可以用作快速沟通渠道!

来自$emit的角度文档:

通过作用域层次向上调度事件名称,通知注册者

由于$rootScope 之上没有范围,因此不会发生冒泡。使用 $rootScope.$emit()/ $rootScope.$on() 作为 EventBus 是完全安全的。

但是,在控制器中使用它时有一个问题。如果您从控制器中直接绑定到$rootScope.$on(),则当您的本地$scope 被破坏时,您必须自己清理绑定。这是因为控制器(与服务相反)可以在应用程序的生命周期内多次实例化,这将导致绑定最终在各处造成内存泄漏:)

要取消注册,只需监听$scope$destroy 事件,然后调用$rootScope.$on 返回的函数。

angular
    .module('MyApp')
    .controller('MyController', ['$scope', '$rootScope', function MyController($scope, $rootScope) {

            var unbind = $rootScope.$on('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });

            $scope.$on('$destroy', unbind);
        }
    ]);

我想说,这并不是一个真正的角度特定的事情,因为它也适用于其他 EventBus 实现,你必须清理资源。

但是,您可以在这些情况下让您的生活更轻松。例如,你可以给 $rootScope 打补丁并给它一个 $onRootScope,它订阅在 $rootScope 上发出的事件,但也可以在本地 $scope 被破坏时直接清理处理程序。

猴子修补$rootScope 以提供这种$onRootScope 方法的最干净的方法是通过装饰器(运行块可能也可以做到这一点,但是pssst,不要告诉任何人)

为了确保$onRootScope 属性在枚举$scope 时不会出现意外,我们使用Object.defineProperty() 并将enumerable 设置为false。请记住,您可能需要 ES5 垫片。

angular
    .module('MyApp')
    .config(['$provide', function($provide){
        $provide.decorator('$rootScope', ['$delegate', function($delegate){

            Object.defineProperty($delegate.constructor.prototype, '$onRootScope', {
                value: function(name, listener){
                    var unsubscribe = $delegate.$on(name, listener);
                    this.$on('$destroy', unsubscribe);

                    return unsubscribe;
                },
                enumerable: false
            });


            return $delegate;
        }]);
    }]);

有了这个方法,上面的控制器代码可以简化为:

angular
    .module('MyApp')
    .controller('MyController', ['$scope', function MyController($scope) {

            $scope.$onRootScope('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });
        }
    ]);

因此,作为所有这一切的最终结果,我强烈建议您使用$rootScope.$emit + $scope.$onRootScope

顺便说一句,我正试图说服 Angular 团队解决 Angular 核心中的问题。这里正在进行讨论:https://github.com/angular/angular.js/issues/4574

这是一个 jsperf,它显示了在只有 100 个 $scope 的体面场景中,$broadcastbrings 的性能影响有多大。

http://jsperf.com/rootscope-emit-vs-rootscope-broadcast

【讨论】:

  • 我正在尝试执行您的第二个选项,但出现错误:未捕获的类型错误:无法重新定义属性:$onRootScope 就在我正在执行 Object.defineProperty 的地方......
  • 也许我把它贴在这里的时候搞砸了。我在生产中使用它,效果很好。我明天去看看:)
  • @Scott 我把它贴过来了,但是代码已经是正确的,并且正是我们在生产中使用的。你能仔细检查一下,你的网站上没有错字吗?我可以在某处查看您的代码以帮助进行故障排除吗?
  • @Christoph 有没有在 IE8 中做装饰器的好方法,因为它不支持非 DOM 对象上的 Object.defineProperty?
  • 这是一个非常聪明的解决问题的方法,但它不再需要了。最新版本的 Angular (1.2.16),可能更早,已经修复了这个问题。现在 $broadcast 不会无缘无故地访问每个后代控制器。它只会访问那些真正在听事件的人。我更新了上面引用的 jsperf 以证明问题现在已解决:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
【解决方案2】:

@zumalifeguard 已经提到,这里的top answer 是针对不再存在的 Angular 问题(至少在 >1.2.16 和“可能更早”的版本中)的解决方法。但是我在没有实际解决方案的情况下阅读所有这些答案。

在我看来,现在的答案应该是

  • 使用$broadcast 来自$rootScope
  • 使用$on收听来自需要了解事件的本地$scope

所以发布

// EXAMPLE PUBLISHER
angular.module('test').controller('CtrlPublish', ['$rootScope', '$scope',
function ($rootScope, $scope) {

  $rootScope.$broadcast('topic', 'message');

}]);

然后订阅

// EXAMPLE SUBSCRIBER
angular.module('test').controller('ctrlSubscribe', ['$scope',
function ($scope) {

  $scope.$on('topic', function (event, arg) { 
    $scope.receiver = 'got your ' + arg;
  });

}]);

小伙伴们

如果你在本地$scope注册监听器,当关联的控制器被移除时,它将是destroyed automatically by $destroy itself

【讨论】:

  • 您知道controllerAs 语法是否可以使用相同的模式?我可以在订阅者中使用$rootScope 来监听事件,但我只是好奇是否有不同的模式。
  • @edhedges 我猜你可以明确地注入$scopeJohn Papa writes 关于事件是他通常将$scope“排除在”他的控制器之外的一个“例外”(我使用引号是因为他提到Controller As 仍然有$scope,它就在引擎盖下)。
  • 在引擎盖下你的意思是你仍然可以通过注射获得它?
  • @edhedges 我根据要求使用controller as 替代语法更新了我的答案。我希望这就是你的意思。
  • @dsdsdsdsd,服务/工厂/供应商将永远存在。 Angular 应用程序中总是只有一个(单例)。另一方面,控制器与功能相关联:组件/指令/ng-controller,它们可以重复(就像由类制成的对象)并且它们根据需要来来去去。当您不再需要某个控件及其控制器时,为什么还要保留它?这就是内存泄漏的定义。
【解决方案3】:

使用 $rootScope.$broadcast 和 $scope.$on 进行 PubSub 通信。

另外,请参阅此帖子:AngularJS – Communicating Between Controllers

【讨论】:

  • 该视频只是对$rootScope$watch 进行了改造。不确定这是否有任何改进。
【解决方案4】:

由于defineProperty存在浏览器兼容性问题,我想我们可以考虑使用服务。

angular.module('myservice', [], function($provide) {
    $provide.factory('msgBus', ['$rootScope', function($rootScope) {
        var msgBus = {};
        msgBus.emitMsg = function(msg) {
        $rootScope.$emit(msg);
        };
        msgBus.onMsg = function(msg, scope, func) {
            var unbind = $rootScope.$on(msg, func);
            scope.$on('$destroy', unbind);
        };
        return msgBus;
    }]);
});

并像这样在控制器中使用它:

  • 控制器 1

    function($scope, msgBus) {
        $scope.sendmsg = function() {
            msgBus.emitMsg('somemsg')
        }
    }
    
  • 控制器 2

    function($scope, msgBus) {
        msgBus.onMsg('somemsg', $scope, function() {
            // your logic
        });
    }
    

【讨论】:

  • +1 表示作用域被破坏时自动取消订阅。
  • 我喜欢这个解决方案。我做了 2 处更改:(1) 允许用户将“数据”传递给发出消息 (2) 使“范围”的传递成为可选的,这样它就可以在单例服务和控制器中使用。您可以在此处查看这些更改:gist.github.com/turtlemonvh/10686980/…
【解决方案5】:

GridLinked 发布了一个PubSub 解决方案,似乎设计得很好。可以找到该服务,here

还有他们的服务图:

【讨论】:

    【解决方案6】:

    实际上使用发射和广播是低效的,因为事件在范围层次结构中上下冒泡,这很容易降低复杂应用程序的性能瓶颈。

    我建议使用服务。这是我最近在我的一个项目中实现它的方式 - https://gist.github.com/3384419

    基本理念 - 将发布订阅/事件总线注册为服务。然后在您需要订阅或发布事件/主题的任何地方注入该事件总线。

    【讨论】:

    • 当不再需要控制器时,如何自动取消订阅?如果你不这样做,由于关闭,控制器将永远不会从内存中删除,你仍然会感知到它的消息。为避免这种情况,您需要手动删除。使用 $on this 不会发生。
    • 这是一个公平的观点。我认为这可以通过您如何构建应用程序来解决。就我而言,我有一个单页应用程序,所以它是一个更容易处理的问题。话虽如此,我认为如果 Angular 有组件生命周期钩子,您可以在其中连接/取消连接这样的东西,这会更干净。
    • 我只是把它留在这里,因为之前没有人说过。将 rootScope 用作 EventBus 并非效率低下,因为 $rootScope.$emit() 只会向上冒泡。但是,由于$rootScope 之上没有范围,所以没有什么好害怕的。因此,如果您只使用$rootScope.$emit()$rootScope.$on(),您将拥有一个快速的系统范围的 EventBus。
    • 您唯一需要注意的是,如果您在控制器中使用$rootScope.$on(),则需要清理事件绑定,否则它们将在创建新绑定时汇总每次实例化控制器时,它们不会为您自动销毁,因为您直接绑定到 $rootScope
    • 最新版本的 Angular (1.2.16),可能更早,已经修复了这个问题。现在 $broadcast 不会无缘无故地访问每个后代控制器。它只会访问那些真正在听事件的人。我更新了上面引用的 jsperf 以证明问题现在已解决:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
    【解决方案7】:

    使用服务中的 get 和 set 方法,您可以非常轻松地在控制器之间传递消息。

    var myApp = angular.module("myApp",[]);
    
    myApp.factory('myFactoryService',function(){
    
    
        var data="";
    
        return{
            setData:function(str){
                data = str;
            },
    
            getData:function(){
                return data;
            }
        }
    
    
    })
    
    
    myApp.controller('FirstController',function($scope,myFactoryService){
        myFactoryService.setData("Im am set in first controller");
    });
    
    
    
    myApp.controller('SecondController',function($scope,myFactoryService){
        $scope.rslt = myFactoryService.getData();
    });
    

    在 HTML HTML 中你可以这样检查

    <div ng-controller='FirstController'>  
    </div>
    
    <div ng-controller='SecondController'>
        {{rslt}}
    </div>
    

    【讨论】:

    • +1 那些显而易见的方法之一 - 非常棒!我已经使用 set(key, value) 和 get(key) 方法实现了一个更通用的版本 - $broadcast 的有用替代方法。
    【解决方案8】:

    关于原始代码 - 您似乎希望在范围之间共享数据。要在 $scope 之间共享数据或状态,文档建议使用服务:

    • 运行跨控制器共享的无状态或有状态代码 — 使用 相反,角度服务。
    • 实例化或管理生命周期 其他组件(例如,创建服务实例)。

    Ref: Angular Docs link here

    【讨论】:

      【解决方案9】:

      我实际上已经开始使用 Postal.js 作为控制器之间的消息总线。

      它作为消息总线有很多好处,例如 AMQP 样式绑定、postal 可以与 iFrame 和 Web 套接字集成的方式,以及更多的东西。

      我使用装饰器在 $scope.$bus 上设置了 Postal...

      angular.module('MyApp')  
      .config(function ($provide) {
          $provide.decorator('$rootScope', ['$delegate', function ($delegate) {
              Object.defineProperty($delegate.constructor.prototype, '$bus', {
                  get: function() {
                      var self = this;
      
                      return {
                          subscribe: function() {
                              var sub = postal.subscribe.apply(postal, arguments);
      
                              self.$on('$destroy',
                              function() {
                                  sub.unsubscribe();
                              });
                          },
                          channel: postal.channel,
                          publish: postal.publish
                      };
                  },
                  enumerable: false
              });
      
              return $delegate;
          }]);
      });
      

      这是有关该主题的博客文章的链接...
      http://jonathancreamer.com/an-angular-event-bus-with-postal-js/

      【讨论】:

        【解决方案10】:

        这就是我使用Factory / Services 和简单的dependency injection (DI) 的方式。

        myApp = angular.module('myApp', [])
        
        # PeopleService holds the "data".
        angular.module('myApp').factory 'PeopleService', ()->
          [
            {name: "Jack"}
          ]
        
        # Controller where PeopleService is injected
        angular.module('myApp').controller 'PersonFormCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
          $scope.people = PeopleService
          $scope.person = {} 
        
          $scope.add = (person)->
            # Simply push some data to service
            PeopleService.push angular.copy(person)
        ]
        
        # ... and again consume it in another controller somewhere...
        angular.module('myApp').controller 'PeopleListCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
          $scope.people = PeopleService
        ]
        

        【讨论】:

        • 你的两个控制器不通信,它们只使用一个相同的服务。那不是一回事。
        • @Greg 您可以通过共享服务并在需要的地方添加 $watches 来用更少的代码实现相同的目标。
        【解决方案11】:

        我喜欢使用$rootscope.emit 实现互通的方式。我建议在不污染全局空间的情况下清洁且性能有效的解决方案。

        module.factory("eventBus",function (){
            var obj = {};
            obj.handlers = {};
            obj.registerEvent = function (eventName,handler){
                if(typeof this.handlers[eventName] == 'undefined'){
                this.handlers[eventName] = [];  
            }       
            this.handlers[eventName].push(handler);
            }
            obj.fireEvent = function (eventName,objData){
               if(this.handlers[eventName]){
                   for(var i=0;i<this.handlers[eventName].length;i++){
                        this.handlers[eventName][i](objData);
                   }
        
               }
            }
            return obj;
        })
        
        //Usage:
        
        //In controller 1 write:
        eventBus.registerEvent('fakeEvent',handler)
        function handler(data){
              alert(data);
        }
        
        //In controller 2 write:
        eventBus.fireEvent('fakeEvent','fakeData');
        

        【讨论】:

        • 对于内存泄漏,您应该添加一个额外的方法来从事件侦听器中注销。反正很好的琐碎样本
        【解决方案12】:

        这是快速而肮脏的方法。

        // Add $injector as a parameter for your controller
        
        function myAngularController($scope,$injector){
        
            $scope.sendorders = function(){
        
               // now you can use $injector to get the 
               // handle of $rootScope and broadcast to all
        
               $injector.get('$rootScope').$broadcast('sinkallships');
        
            };
        
        }
        

        这是一个添加到任何同级控制器中的示例函数:

        $scope.$on('sinkallships', function() {
        
            alert('Sink that ship!');                       
        
        });
        

        当然还有你的 HTML:

        <button ngclick="sendorders()">Sink Enemy Ships</button>
        

        【讨论】:

        • 你为什么不直接注入$rootScope
        【解决方案13】:

        从 Angular 1.5 开始,它是基于组件的开发重点。组件交互的推荐方式是使用“require”属性和属性绑定(输入/输出)。

        一个组件需要另一个组件(例如根组件)并获得对其控制器的引用:

        angular.module('app').component('book', {
            bindings: {},
            require: {api: '^app'},
            template: 'Product page of the book: ES6 - The Essentials',
            controller: controller
        });
        

        然后你可以在你的子组件中使用根组件的方法:

        $ctrl.api.addWatchedBook('ES6 - The Essentials');
        

        这是根组件控制器函数:

        function addWatchedBook(bookName){
        
          booksWatched.push(bookName);
        
        }
        

        这是一个完整的架构概述:Component Communications

        【讨论】:

          【解决方案14】:

          你可以在模块的任何地方访问这个hello函数

          控制器一

           $scope.save = function() {
              $scope.hello();
            }
          

          第二个控制器

            $rootScope.hello = function() {
              console.log('hello');
            }
          

          More info here

          【讨论】:

          • 聚会有点晚了,但是:不要这样做。将函数置于根作用域类似于将函数设为全局,这可能会导致各种问题。
          【解决方案15】:

          我将创建一个服务并使用通知。

          1. 在通知服务中创建一个方法
          2. 创建一个在 Notification Service 中广播通知的通用方法。
          3. 从源控制器调用notificationService.Method。如果需要,我还会传递相应的对象以持久化。
          4. 在方法中,我将数据持久化到通知服务中并调用通用通知方法。
          5. 在目标控制器中,我侦听 ($scope.on) 广播事件并从通知服务访问数据。

          在任何时候通知服务都是单例的,它应该能够提供持久的数据。

          希望对你有帮助

          【讨论】:

            【解决方案16】:

            您可以使用 AngularJS 内置服务 $rootScope 并将此服务注入您的两个控制器中。 然后,您可以侦听在 $rootScope 对象上触发的事件。

            $rootScope 提供了两个名为$emit and $broadcast 的事件调度器,它们负责调度事件(可能是自定义事件),并使用$rootScope.$on 函数添加事件监听器。

            【讨论】:

              【解决方案17】:

              您应该使用 Service ,因为 $rootscope 是从整个 Application 访问,它会增加负载,或者如果您的数据不多,您可以使用 rootparams。

              【讨论】:

                【解决方案18】:
                function mySrvc() {
                  var callback = function() {
                
                  }
                  return {
                    onSaveClick: function(fn) {
                      callback = fn;
                    },
                    fireSaveClick: function(data) {
                      callback(data);
                    }
                  }
                }
                
                function controllerA($scope, mySrvc) {
                  mySrvc.onSaveClick(function(data) {
                    console.log(data)
                  })
                }
                
                function controllerB($scope, mySrvc) {
                  mySrvc.fireSaveClick(data);
                }
                

                【讨论】:

                  【解决方案19】:

                  您可以使用 $emit 和 $broadcast 等角度事件来实现。据我们所知,这是最好、最有效和最有效的方法。

                  首先我们从一个控制器调用一个函数。

                  var myApp = angular.module('sample', []);
                  myApp.controller('firstCtrl', function($scope) {
                      $scope.sum = function() {
                          $scope.$emit('sumTwoNumber', [1, 2]);
                      };
                  });
                  myApp.controller('secondCtrl', function($scope) {
                      $scope.$on('sumTwoNumber', function(e, data) {
                          var sum = 0;
                          for (var a = 0; a < data.length; a++) {
                              sum = sum + data[a];
                          }
                          console.log('event working', sum);
                  
                      });
                  });
                  

                  您也可以使用 $rootScope 代替 $scope。相应地使用您的控制器。

                  【讨论】:

                    猜你喜欢
                    • 2015-03-23
                    • 1970-01-01
                    • 2019-06-28
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-03-03
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多