Как правильно общаться между контроллерами в AngularJS?

449

Каков правильный способ связи между контроллерами?

В настоящее время я использую ужасное выдумку с участием 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);
    }
}
  • 34
    Полностью спорный, но в Angular вы всегда должны использовать $ window вместо собственного объекта окна JS. Таким образом, вы можете заглушить это в своих тестах :)
  • 1
    Пожалуйста, смотрите комментарий в ответе от меня по этому вопросу. $ широковещание уже не дороже, чем $ emit. Смотрите ссылку на jsperf, на которую я ссылался.
Теги:
scope

19 ответов

420
Лучший ответ

Изменить. Проблема, затронутая в этом ответе, решена в angular.js версии 1.2.7. $broadcast теперь позволяет избежать пузырьков по незарегистрированным областям и работает так же быстро, как и $emit. Изображение 3648

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

  • используйте $broadcast из $rootScope
  • прослушивайте с помощью $on из локального $scope, который должен знать о событии

Оригинальный ответ ниже

Я настоятельно рекомендую не использовать $rootScope.$broadcast + $scope.$on, а скорее $rootScope.$emit + $rootScope.$on. Первый может вызвать серьезные проблемы с производительностью, вызванные @numan. Это связано с тем, что событие будет проходить через области все.

Однако последний (используя $rootScope.$emit + $rootScope.$on) страдает от не и поэтому может быть использован как быстрый канал связи!

Из angular документации $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);
        }
    ]);

Я бы сказал, что на самом деле не какая-то особенность angular, так как она применима и к другим реализациям 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.

Btw, я пытаюсь убедить команду angular решить проблему в ядре angular. Здесь идет обсуждение: https://github.com/angular/angular.js/issues/4574

Вот jsperf, который показывает, сколько из перфоманса $broadcast приводит к таблице в достойном сценарии всего за 100 $scope.

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

Изображение 3649

  • 0
    Я пытаюсь сделать ваш второй вариант, но получаю ошибку: Uncaught TypeError: Невозможно переопределить свойство: $ onRootScope там, где я делаю Object.defineProperty ....
  • 0
    Может быть, я что-то напортачил, когда вставил это сюда. Я использую его в производстве, и он прекрасно работает. Завтра посмотрю :)
Показать ещё 15 комментариев
91

top answer здесь была проблема с проблемой 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 , он просто находится под капотом).
Показать ещё 6 комментариев
54

Используя $rootScope. $broadcast и $scope. $on для связи PubSub.

Также см. этот пост: AngularJS - Связь между контроллерами

  • 3
    Это видео просто заново изобретает $rootScope и $watch . Не уверен, что это улучшение.
44

Поскольку у 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/…
21

GridLinked разместил решение PubSub, которое кажется который будет хорошо разработан. Сервис можно найти здесь.

Также показана схема их обслуживания:

Изображение 3650

15

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

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

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

  • 7
    И когда контроллер больше не нужен, как вы автоматически отписываетесь от него? Если вы этого не сделаете, из-за закрытия контроллер никогда не будет удален из памяти, и вы все равно будете воспринимать ему сообщения. Чтобы избежать этого вам нужно будет удалить потом вручную. Использование $ на этом не произойдет.
  • 1
    это справедливо. я думаю, что это может быть решено тем, как вы разрабатываете свое приложение. в моем случае у меня есть одностраничное приложение, так что это более сложная проблема. Сказав это, я думаю, что это было бы намного чище, если бы у angular были зацепки жизненного цикла компонентов, где вы могли бы подключать / подключать такие вещи.
Показать ещё 4 комментария
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
    +1 Один из тех очевидных, когда ты говоришь, методов - отлично! Я реализовал более общую версию с методами set (key, value) и get (key) - полезная альтернатива $ broadcast.
8

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

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

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

5

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

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

Я использовал декоратор, чтобы настроить Postal на $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

Мне понравилось, как $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
    Для утечки памяти вы должны добавить дополнительный метод для отмены регистрации от слушателей событий. В любом случае хороший тривиальный образец
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
    Ваши два контроллера не общаются, они используют только один и тот же сервис. Это не одно и то же.
  • 0
    @ Грег, вы можете добиться того же с меньшим количеством кода, если будете использовать общую службу и добавлять $ watches, где это необходимо.
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 ?
0

Вы можете сделать это, используя события angular, которые являются $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. Используйте ваш контроллер соответственно.

0

Запуск 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
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

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

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

0

Вы должны использовать Сервис, потому что $rootscope - это доступ из всего приложения, и он увеличивает нагрузку, или вы используете корневые патчи, если ваши данные не больше.

0

Я создам службу и использую уведомление.

  • Создайте метод в службе уведомлений
  • Создайте общий метод для трансляции уведомлений в службе уведомлений.
  • Из исходного контроллера вызывается метод notificationService.Method. Я также передаю соответствующий объект для сохранения, если это необходимо.
  • Внутри метода я сохраняю данные в службе уведомлений и вызываю общий метод уведомления.
  • В контроллере назначения я слушаю ($ scope.on) для широковещательного события и получаю данные из службы уведомлений.

Как и в любой точке службы Notification Service, она должна обеспечивать постоянную передачу данных.

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

0

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

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

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

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

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

Дополнительная информация здесь

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню