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


473

控制器之间进行通讯的正确方法是什么?

我目前正在使用涉及到的可怕软糖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);
    }
}

36
完全没有意义,但在Angular中,应始终使用$ window而不是本机JS窗口对象。这样,您就可以在测试中将其存根:)
Dan M

1
请参阅以下有关此问题的答复中的评论。$ broadcast不再比$ emit贵。请参阅我在此处引用的jsperf链接。
zumalifeguard 2014年

Answers:


457

编辑:此答案中解决的问题已在angular.js 版本1.2.7中解决。$broadcast现在可以避免在未注册的作用域上冒泡,并且运行速度与$ emit一样快。 $ broadcast表演与角度为1.2.16的$ 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 是完全安全的。

但是,从Controllers中使用它时只有一个陷阱。如果直接$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,以订阅on发出的事件,$rootScope但也可以在本地$scope被销毁时直接清理处理程序。

$rootScope提供这种$onRootScope方法的最简单的方法就是通过装饰器(运行块也可以做到这一点,但是pssst,不要告诉任何人)

为确保该$onRootScope属性在枚举时不会出现意外情况,$scope我们使用Object.defineProperty()并将其设置enumerablefalse。请记住,您可能需要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

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

这是一个jsperf,它显示了$broadcast在只有100的体面场景中,性能影响带来的影响$scope

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

jsperf结果


我试图做你的第二个选项,但我得到一个错误:未捕获的类型错误:不能重定义属性:$ onRootScope身在何方我做的Object.defineProperty ....
斯科特

也许我在这里粘贴东西时搞砸了。我在生产中使用它,效果很好。我明天再看:)
Christoph

@Scott我将其粘贴了,但是代码已经正确,并且正是我们在生产中使用的代码。您可以仔细检查一下,确认您的网站上没有错字吗?我可以在某处看到您的代码以帮助进行故障排除吗?
Christoph

@Christoph在IE8中有一种做装饰器的好方法,因为它不支持非DOM对象上的Object.defineProperty?
joshschreuder 2014年

59
这是解决问题的非常聪明的方法,但是不再需要。Angular(1.2.16)的最新版本(可能更早)已解决此问题。现在$ broadcast不会无缘无故访问每个后代控制器。它只会访问实际在听事件的人。我更新了上面引用的jsperf,以证明该问题现已解决:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard 2014年

107

顶部的答案在这里是从不再存在(至少在版本> 1.2.16和“可能更早”)作为@zumalifeguard提到的角度问题,周围的工作。但是我没有阅读所有这些答案,而没有实际的解决方案。

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

  • 使用$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,它会自动销毁$destroy本身时,相关的控制器被删除。


1
您知道controllerAs语法是否可以使用相同的模式吗?我可以$rootScope在订户中使用它来监听事件,但是我只是好奇是否存在其他模式。
edhedges 2014年

3
@edhedges我猜您可以$scope显式注入。约翰爸爸写有关事件是一个“例外”,以他一贯保持的规则,$scope“走出去”的他控制器(我用引号,因为他提到Controller As还是有$scope,只是引擎盖下的)。
豪华的2014年

在引擎盖下,您是说仍然可以通过注射达到目标?
edhedges 2014年

2
@edhedges我controller as根据要求使用语法替代方法更新了答案。我希望那是你的意思。
豪华的

3
@dsdsdsdsd,服务/工厂/提供者将永远存在。在Angular应用程序中,总是只有一个(单子集)。另一方面,控制器与功能相关:组件/指令/ ng-controller,可以重复使用(就像从类中创建的对象一样),它们可以根据需要来来去去。为什么当您不再需要某个控件及其控制器时,为什么要保留它?那就是内存泄漏的定义。
豪华的


42

由于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
范围被销毁时,+ 1表示自动取消订阅。
Federico Nafria 2014年

6
我喜欢这个解决方案。我做了2项更改:(1)允许用户将“数据”传递给发射消息(2)使“作用域”的传递成为可选项,因此可以在单例服务和控制器中使用。你可以看到这些变化实现在这里:gist.github.com/turtlemonvh/10686980/...
turtlemonvh


15

实际上,使用发射和广播效率不高,因为事件在作用域层次结构中上下起泡,对于复杂的应用程序而言,事件层次结构很容易降低性能。

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

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


7
当不再需要控制器时,如何自动取消订阅呢?如果您不这样做,由于关闭,控制器将永远不会从内存中删除,并且您仍然会感知到发送给它的消息。为避免这种情况,您将需要手动删除。使用$ on不会发生。
Renan Tomal Fernandes 2012年

1
多数民众赞成在一个公平的点。我认为可以通过您如何构建应用程序来解决。就我而言,我只有一个页面应用程序,因此它是一个更易于处理的问题。话虽如此,但我认为如果angular拥有组件生命周期挂钩,您可以在其中进行诸如此类的接线/断开接线,这将更加清洁。
努曼·萨拉提

6
我只是把这留在这里,因为以前没有人说过。将rootScope用作EventBus 并非没有效率,因为$rootScope.$emit()只会向上冒泡。但是,由于上面没有范围,$rootScope因此无需担心。因此,如果您只是在使用$rootScope.$emit()$rootScope.$on()您将拥有一个快速的系统级EventBus。
克里斯多夫(Christoph)

1
您唯一需要了解的是,如果您$rootScope.$on()在控制器内部使用,则需要清理事件绑定,否则它们将在每次实例化控制器时创建一个新的事件时进行汇总。由于您$rootScope直接绑定,因此会自动为您销毁。
克里斯多夫(Christoph)

Angular(1.2.16)的最新版本(可能更早)已解决此问题。现在$ broadcast不会无缘无故访问每个后代控制器。它只会访问实际在听事件的人。我更新了上面引用的jsperf,以证明该问题现已解决:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard 2014年

14

在服务中使用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的有用替代方法。
TonyWilk,2016年

8

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

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

参考:Angular Docs链接在这里


5

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

作为消息总线,它有很多好处,例如AMQP样式绑定,邮政可以集成w / 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/


3

这就是我使用Factory / Service和简单依赖项注入(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通过共享服务并在需要的地方添加$ watches,可以用更少的代码实现相同的目的。
卡帕杰2015年

3

我喜欢$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');

对于内存泄漏,您应该添加一个额外的方法来从事件侦听器中注销。无论如何,都是好样本
Raffaeu

2

这是一种快速而肮脏的方法。

// 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
你为什么不注射$rootScope
Pieter Herroelen 2014年

1

从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

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

控制器一

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

第二控制器

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

更多信息在这里


7
派对晚了一点,但是:不要这样做。将函数放在根作用域上类似于将函数全局化,这可能会导致各种问题。
丹·潘特里

0

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

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

由于Notification Service在任何时候都是单例的,因此应该能够提供持久化的数据。

希望这可以帮助


0

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

$ rootScope提供了两个称为的事件分派器$emit and $broadcast,它们负责分派事件(可以是自定义事件),并使用$rootScope.$on函数添加事件侦听器。


0

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


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

0

您可以通过使用$ 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。相应地使用您的控制器。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.