Как правильно общаться между контроллерами в 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. Таким образом, вы можете заглушить это в своих тестах :)
Дан М

1
Пожалуйста, смотрите комментарий в ответе от меня по этому вопросу. $ трансляция уже не дороже, чем $ emit. Смотрите ссылку на jsperf, на которую я ссылался.
zumalifeguard

Ответы:


457

Редактировать : проблема, затронутая в этом ответе, была решена в angular.js версии 1.2.7 . $broadcastтеперь избегает пузырей над незарегистрированными областями и работает так же быстро, как $ emit. $ трансляционные характеристики идентичны $ emit с угловым 1.2.16

Итак, теперь вы можете:

  • использовать $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«S $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метода был бы через декоратор (блок run, вероятно, тоже отлично это сделает, но 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, который показывает, сколько перфектного воздействия $broadcastприносит на стол при достойном сценарии всего за 100 $scopeс.

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

jsperf результаты


Я пытаюсь сделать ваш второй вариант, но получаю ошибку: Uncaught TypeError: Невозможно переопределить свойство: $ onRootScope там, где я делаю Object.defineProperty ....
Скотт

Может быть, я что-то напортачил, когда вставил это сюда. Я использую его в производстве, и он прекрасно работает. Завтра посмотрю :)
Кристоф

@ Скотт Я вставил его, но код уже был верным и это именно то, что мы используем в производстве. Можете ли вы проверить, что на вашем сайте нет опечатки? Могу ли я где-нибудь увидеть ваш код для устранения неполадок?
Кристоф

@Christoph Есть ли хороший способ сделать декоратор в IE8, так как он не поддерживает Object.defineProperty для объектов не-DOM?
Joshschreuder

59
Это было очень умное решение проблемы, но оно больше не нужно. В последней версии Angular (1.2.16) и, возможно, ранее, эта проблема исправлена. Теперь $ широковещание не будет посещать каждый контроллер потомка не по причине. Он будет посещать только тех, кто на самом деле слушает событие. Я обновил jsperf, упомянутый выше, чтобы продемонстрировать, что проблема теперь исправлена: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

Верх ответ здесь был Обойти от Angular проблемы , которая больше не существует (по крайней мере , в версии> 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;
  });

}]);

Plunkers

Если зарегистрировать слушатель на локальном $scope, он будет уничтожен автоматически $destroyсам по себе , когда соответствующему контроллеру удаляется.


1
Вы знаете, может ли этот же шаблон использоваться с controllerAsсинтаксисом? Я мог использовать $rootScopeподписчика для прослушивания события, но мне было просто любопытно, был ли другой образец.
хеджирует

3
@edhedges Я думаю, что вы могли бы ввести $scopeявно. Джон Папа пишет о событиях быть один «исключением» к своему обычному правилу учета $scope«из» своих контроллеров (я использую кавычки , потому что , как он упоминает до Controller Asсих пор $scope, это только под капотом).
самое лучшее

Под капотом вы имеете в виду, что вы все еще можете получить его путем инъекции?
выровняется

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 за автоматическую отписку при уничтожении области.
Федерико Нафриа

6
Мне нравится это решение. 2 изменения, которые я сделал: (1) разрешить пользователю передавать «данные» в сообщение emit (2) сделать передачу «scope» необязательной, чтобы ее можно было использовать в одноэлементных службах, а также в контроллерах. Вы можете увидеть эти изменения здесь: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

На самом деле использование emit и broadcast неэффективно, потому что событие всплывает вверх и вниз по иерархии областей действия, что может легко перерасти в снижение производительности для сложного приложения.

Я бы предложил воспользоваться услугой. Вот как я недавно реализовал это в одном из моих проектов - https://gist.github.com/3384419 .

Основная идея - зарегистрировать pubsub / event bus как сервис. Затем внедрите этот eventbus туда, где вам нужно подписаться или опубликовать события / темы.


7
И когда контроллер больше не нужен, как вы автоматически отписываетесь от него? Если вы этого не сделаете, из-за закрытия контроллер никогда не будет удален из памяти, и вы все равно будете воспринимать ему сообщения. Чтобы избежать этого вам нужно будет удалить потом вручную. Использование $ на этом не произойдет.
Ренан Томаль Фернандес

1
это справедливо. я думаю, что это может быть решено тем, как вы разрабатываете свое приложение. в моем случае у меня есть одностраничное приложение, так что это более сложная проблема. Сказав это, я думаю, что было бы намного чище, если бы у angular были хуки компонентов жизненного цикла, где вы могли бы связывать / разводить подобные вещи.
numan salati

6
Я просто оставляю это здесь, так как никто не говорил об этом раньше. Использование rootScope в качестве EventBus не является неэффективным, поскольку $rootScope.$emit()только пузырьки вверх. Тем не менее, так как нет никаких оснований, $rootScopeбояться нечего. Так что, если вы просто используете, $rootScope.$emit()и у $rootScope.$on()вас будет быстрый системный EventBus.
Кристоф

1
Единственное, о чем вам нужно знать, это то, что если вы используете $rootScope.$on()внутри своего контроллера, вам нужно будет очистить привязку события, иначе они будут суммироваться, поскольку при каждом создании экземпляра контроллера создается новое, а они - нет. получить автоматически уничтожены для вас, так как вы привязаны к $rootScopeнапрямую.
Кристоф

В последней версии Angular (1.2.16) и, возможно, ранее, эта проблема исправлена. Теперь $ широковещание не будет посещать каждый контроллер потомка не по причине. Он будет посещать только тех, кто на самом деле слушает событие. Я обновил jsperf, упомянутый выше, чтобы продемонстрировать, что проблема теперь исправлена: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

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

8

Что касается исходного кода - кажется, вы хотите поделиться данными между областями. Чтобы поделиться данными или состоянием между $ scope, документы предлагают использовать сервис:

  • Для запуска кода без сохранения состояния или с состоянием, совместно используемого контроллерами - используйте угловые сервисы.
  • Для создания экземпляра или управления жизненным циклом других компонентов (например, для создания экземпляров службы).

Ссылка: угловые документы ссылка здесь


5

Я фактически начал использовать Postal.js в качестве шины сообщений между контроллерами.

В этом есть много преимуществ, таких как шина сообщений, такая как привязки в стиле AMQP, способ, которым почтовые службы могут интегрировать w / iFrames и веб-сокеты, и многое другое.

Я использовал декоратор, чтобы настроить Почту на $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 / Services и простым внедрением зависимостей (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
Ваши два контроллера не общаются, они используют только один и тот же сервис. Это не одно и то же.
Грег

@ Грег, вы можете добиться того же с меньшим количеством кода, если будете использовать общую службу и добавлять $ watches, где это необходимо.
Капай

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

Для утечки памяти вы должны добавить дополнительный метод для отмены регистрации от слушателей событий. В любом случае хороший тривиальный образец
Раффау

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?
Питер Херроэлен

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

Вы можете получить доступ к этой функции привет в любом месте модуля

Контроллер один

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

второй контроллер

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

Больше информации здесь


7
Немного опоздал на вечеринку, но: не делай этого. Помещение функции в корневую область видимости похоже на создание глобальной функции, которая может вызвать всевозможные проблемы.
Дэн Кладовая

0

Я создам сервис и буду использовать уведомления.

  1. Создать метод в службе уведомлений
  2. Создайте общий метод для широковещательного уведомления в службе уведомлений.
  3. Из контроллера источника вызовите уведомление Service.Method. Я также передаю соответствующий объект для сохранения в случае необходимости.
  4. Внутри метода я сохраняю данные в службе уведомлений и вызываю общий метод уведомления.
  5. В контроллере назначения я прослушиваю ($ scope.on) широковещательное событие и получаю доступ к данным из службы уведомлений.

Поскольку в любой момент служба уведомлений является одноуровневой, она должна иметь возможность предоставлять постоянные данные.

Надеюсь это поможет


0

Вы можете использовать встроенную службу AngularJS $rootScopeи внедрить эту службу в оба ваших контроллера. Затем вы можете прослушивать события, которые запускаются на объекте $ rootScope.

$ rootScope предоставляет два вызываемых $emit and $broadcastдиспетчера событий, которые отвечают за диспетчеризацию событий (могут быть пользовательскими событиями) и используют $rootScope.$onфункцию для добавления прослушивателя событий.


0

Вы должны использовать Сервис, потому что $rootscopeэто доступ из всего Приложения, и это увеличивает нагрузку, или вы можете использовать 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. Используйте свой контроллер соответственно.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.