在 AngularJS 中控制器之间进行通信的正确方式是什么?

What's the correct way to communicate between controllers in AngularJS?

提问人:fadedbee 提问时间:6/29/2012 最后编辑:Shashank Agrawalfadedbee 更新时间:6/22/2017 访问量:217074

问:

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

我目前正在使用一个可怕的软糖,涉及: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);
    }
}
作用域 angularjs

评论

36赞 Dan M 11/26/2012
完全没有意义,但在 Angular 中,您应该始终使用 $window 而不是原生 JS 窗口对象。这样,您就可以在测试中将其存根:)
1赞 zumalifeguard 5/8/2014
请参阅下面我关于这个问题的回答中的评论。$broadcast不再比$emit贵。请参阅我在那里引用的 jsperf 链接。

答:

54赞 Renan Tomal Fernandes 6/29/2012 #1

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

另外,请参阅这篇文章: AngularJS – 控制器之间的通信

评论

3赞 nilskp 4/9/2013
该视频只是重新发明和.不确定是否有任何改进。$rootScope$watch
20赞 Ryan Schumacher 7/11/2012 #2

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

还有他们的服务图:

Messaging Service

15赞 numan salati 8/18/2012 #3

实际上,使用 emit 和 broadcast 效率低下,因为事件在作用域层次结构中上下冒泡,这很容易降低到复杂应用程序的性能瓶颈。

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

基本思想 - 将 pubsub/event 总线注册为服务。然后将该事件总线注入到需要订阅或发布事件/主题的任何位置。

评论

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

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

  • 运行在控制器之间共享的无状态或有状态代码 — 使用 相反,angular 服务。
  • 实例化或管理 其他组件(例如,用于创建服务实例)。

参考:Angular Docs 链接在这里

2赞 Peter Drinnan 7/31/2013 #5

这是快速而肮脏的方法。

// 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>

评论

16赞 Pieter Herroelen 4/29/2014
你为什么不直接注射?$rootScope
460赞 20 revs, 7 users 87%Christoph #6

编辑:此答案中解决的问题已在 angular.js 版本 1.2.7 中得到解决。 现在避免了在未注册的范围上冒泡,并且运行速度与$emit一样快。$broadcast$broadcast performances are identical to $emit with angular 1.2.16

因此,现在您可以:

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

原始答案如下

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

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

从 angular 文档:$emit

通过作用域层次结构向上调度事件名称,通知已注册的事件

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

但是,在控制器中使用它时有一个问题。如果直接从控制器内绑定到控制器,则当本地绑定被销毁时,必须自行清理绑定。这是因为控制器(与服务相比)可以在应用程序的生命周期内多次实例化,这将导致绑定相加,最终到处造成内存泄漏:)$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

提供这种方法的最干净的方法是通过装饰器(运行块也可能做得很好,但 pssst,不要告诉任何人)$rootScope$onRootScope

为了确保该属性在枚举时不会意外显示,我们使用 并设置为 。请记住,您可能需要一个 ES5 垫片。$onRootScope$scopeObject.defineProperty()enumerablefalse

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 个的体面场景中,性能影响会带来多大的影响。$broadcast$scope

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

jsperf results

评论

0赞 Scott 11/7/2013
我正在尝试执行您的第二个选项,但是我收到一个错误:Uncaught TypeError:无法重新定义属性:$onRootScope我正在执行Object.defineProperty...。
0赞 Christoph 11/8/2013
也许当我把它粘贴到这里时,我搞砸了一些东西。我在生产中使用它,效果很好。我明天去看看:)
0赞 Christoph 11/8/2013
@Scott我把它粘贴过来了,但代码已经是正确的,并且正是我们在生产中使用的代码。你能仔细检查一下,你的网站上没有错别字吗?我可以在某处看到您的代码以帮助进行故障排除吗?
0赞 joshschreuder 1/7/2014
@Christoph IE8 中有什么好的方法可以做装饰器,因为它不支持非 DOM 对象上的 Object.defineProperty?
59赞 zumalifeguard 5/8/2014
这是一个非常聪明的解决问题的方法,但不再需要它了。最新版本的 Angular (1.2.16) 以及更早版本已修复此问题。现在$broadcast不会无缘无故地访问每个后代控制器。它只会访问那些真正在听活动的人。我更新了上面引用的 jsperf 以证明问题现已修复:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
42赞 Singo 2/17/2014 #7

由于 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
        });
    }
    

评论

7赞 Federico Nafria 3/20/2014
+1 表示范围被销毁时的自动退订。
6赞 turtlemonvh 4/15/2014
我喜欢这个解决方案。我所做的 2 项更改:(1) 允许用户将“数据”传递给发出消息 (2) 使“scope”的传递成为可选的,以便可以在单例服务和控制器中使用。您可以在此处看到这些更改的实施:gist.github.com/turtlemonvh/10686980/...
0赞 Prashobh 3/18/2014 #8

您可以在模块中的任何位置访问此 hello 函数

控制器一

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

第二个控制器

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

更多信息请点击这里

评论

7赞 Dan 7/22/2015
聚会有点晚了,但是:不要这样做。将函数放在根作用域上类似于使函数全局化,这可能会导致各种问题。
5赞 jcreamer898 5/8/2014 #9

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

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

我使用装饰器来设置邮政......$scope.$bus

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/

0赞 rahul 6/6/2014 #10

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

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

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

希望这会有所帮助

3赞 Oto Brglez 7/31/2014 #11

这就是我使用工厂/服务和简单依赖注入 (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
]

评论

1赞 Greg 8/28/2014
您的两个控制器不通信,它们只使用相同的服务。那不是一回事。
0赞 Capaj 3/25/2015
@Greg,您可以通过共享服务并在需要时添加$watches,用更少的代码实现相同的目标。
109赞 poshest 9/26/2014 #12

这里的最高答案是解决 Angular 问题,该问题不再存在(至少在版本 >1.2.16 和“可能更早”中),正如@zumalifeguard提到的。但是我只能阅读所有这些答案,而没有实际的解决方案。

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

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

所以要发布

// 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;
  });

}]);

Plunkers

如果在本地上注册侦听器,则当删除关联的控制器时,$destroy本身将自动销毁该侦听器。$scope

评论

1赞 edhedges 9/30/2014
您知道是否可以将相同的模式与语法一起使用吗?我能够在订阅者中使用来收听事件,但我只是好奇是否有不同的模式。controllerAs$rootScope
3赞 poshest 9/30/2014
@edhedges 我想你可以明确地注入。约翰·帕帕(John Papa)写道,事件是他通常将控制器“排除在外”的规则的一个“例外”(我使用引号是因为正如他提到的仍然有,它就在引擎盖下)。$scope$scopeController As$scope
0赞 edhedges 10/1/2014
在引擎盖下,你的意思是你仍然可以通过注射来获得它吗?
2赞 poshest 10/1/2014
@edhedges我按照要求用语法替代更新了我的答案。我希望这就是你的意思。controller as
3赞 poshest 3/15/2016
@dsdsdsdsd,服务/工厂/供应商将永远存在。在 Angular 应用程序中,总是有一个且只有一个(单例)。另一方面,控制器与功能相关联:组件/指令/ng-controller,它们可以重复(例如由类生成的对象),并且它们根据需要来来去去。为什么你希望一个控件及其控制器在你不再需要它时保持存在?这就是内存泄漏的定义。
14赞 Load Reconn 10/14/2014 #13

在服务中使用 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>

评论

0赞 TonyWilk 10/20/2016
+1 那些显而易见的“一旦你被告知”的方法之一 - 太棒了!我使用 set( key, value) 和 get(key) 方法实现了一个更通用的版本 - 这是 $broadcast 的有用替代方案。
0赞 abhaygarg12493 12/29/2014 #14

您应该使用服务,因为它是从整个应用程序访问的,并且会增加负载,或者如果您的数据不多,则可以使用rootparams。$rootscope

0赞 Shivang Gupta 12/30/2014 #15

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

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

0赞 Amin Rahimi 3/12/2015 #16
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);
}
3赞 shikhar chauhan 10/4/2015 #17

我喜欢如何实现相互交流的方式。我建议在不污染全球空间的情况下使用清洁且性能有效的解决方案。$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');

评论

0赞 Raffaeu 11/15/2016
对于内存泄漏,您应该添加一个额外的方法来从事件侦听器中取消注册。无论如何,很好的琐碎样本
1赞 kevinius 11/24/2016 #18

从 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);

}

下面是一个完整的架构概述: 组件通信

0赞 Peeyush Kumar 5/25/2017 #19

您可以使用 $emit 和 $broadcast 的 angular 事件来做到这一点。据我们所知,这是最好、最高效和最有效的方法。

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

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。相应地使用您的控制器。