Який правильний спосіб спілкування між контролерами в 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
Будь ласка, дивіться коментар у відповіді нижче від мене стосовно цього питання. $ трансляція вже не дорожча, ніж $ 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, $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.

До речі, я намагаюсь переконати команду кутових вирішити проблему в кутовому ядрі. Тут триває дискусія: https://github.com/angular/angular.js/isissue/4574

Ось jsperf, який показує, скільки впливів на парфюмер $broadcastприносить на стіл у пристойному сценарії всього з 100 $scope.

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

jsperf результати


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

Можливо, я щось накрутив, коли вклеїв його сюди. Я використовую його у виробництві, і він чудово працює. Я загляну завтра :)
Крістоф

@Scott Я вставив його, але код вже був правильним і саме це ми використовуємо у виробництві. Чи можете ви двічі перевірити, чи немає на вашому сайті помилкової помилки? Чи можу я десь побачити ваш код, щоб допомогти вирішити проблеми?
Крістоф

@Christoph чи є хороший спосіб зробити декоратор в IE8, оскільки він не підтримує Object.defineProperty на об'єктах, які не є DOM?
joshschreuder

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

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в передплатнику для прослуховування події, але мені було просто цікаво, чи існує інша картина.
краями

3
@edhedges Я думаю, ви могли б ввести $scopeявно. Джон Папа пише про те, що події є одним із "винятків" зі свого звичайного правила $scope"відмовлятися" від своїх контролерів (я використовую цитати, тому що, як він згадує, Controller Asдосі $scope, це просто під капотом).
найвищий

Під капелюшком ви маєте на увазі, що ви все ще можете потрапити на нього через ін'єкцію?
краями

2
@edhedges Я оновив свою відповідь controller asальтернативою синтаксису за потребою . Я сподіваюся, що це ви мали на увазі.
найвищий

3
@dsdsdsdsd, послуги / фабрики / постачальники залишатимуться назавжди. У програмі Angular завжди є одна і лише одна (одиночні кнопки). З іншого боку, контролери пов'язані з функціональністю: компоненти / директиви / ng-контролери, які можна повторювати (як об'єкти, зроблені з класу), і вони надходять і йдуть у міру необхідності. Чому ви хочете, щоб контроль і його контролер залишалися існуючими, коли він вам більше не потрібен? Це саме визначення витоку пам’яті.
Остання


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) дозволяю користувачеві передавати "дані" в повідомлення про випромінення (2) робити передачу "області" необов'язковою, щоб це можна було використовувати як в одиночних службах, так і в контролерах. Ці зміни ви можете переглянути тут: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

Насправді використання випромінювання та трансляції неефективне, оскільки подія перетворює вгору та вниз ієрархію сфери, яка може легко погіршитись у розширення продуктивності для складного застосування.

Я б запропонував скористатися послугою. Ось як я нещодавно реалізував це в одному зі своїх проектів - https://gist.github.com/3384419 .

Основна ідея - зареєструвати автобус pubsub / event як послугу. Потім введіть цей автобус подій, куди вам потрібно підписатись або опублікувати події / теми.


7
А коли контролер більше не потрібен, як ви автоматично його скасуєте? Якщо цього не зробити, через закриття контролер ніколи не буде видалений з пам'яті, і ви все одно будете чути повідомлення до нього. Щоб уникнути цього, вам потрібно буде видалити його вручну. Використання $ на цьому не відбудеться.
Ренан Томал Фернандес

1
ось чесна справа. Я думаю, що це може бути вирішено тим, як ви архітектуру вашої програми. у моєму випадку у мене є додаток на одній сторінці, тож це проблема, яка є більш керованою. сказавши це, я думаю, що це було б набагато чистіше, якби у кутових були гачки життєвого циклу компонентів, де ви могли б проводити / відмовляти подібні речі.
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

Використовуючи методи отримання та встановлення в межах послуги, ви можете легко передавати повідомлення між контролерами.

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 (ключ, значення) та get (key) - корисна альтернатива $ Broadcast.
TonyWilk

8

Що стосується оригінального коду - здається, ви хочете поділитися даними між областями. Щоб поділитись Даними або Штатом між областями $, документи пропонують скористатися послугою:

  • Для запуску коду без стану або штатного коду, що ділиться між контролерами - замість цього використовуйте кутові служби
  • Інстанціювати або керувати життєвим циклом інших компонентів (наприклад, для створення службових примірників).

Ref: Кутове посилання на документи тут


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
Ваші два контролери не спілкуються, вони використовують лише одну і ту ж послугу. Це не те саме.
Грег

@Greg ви можете досягти того ж, використовуючи менший код, маючи спільний сервіс та додаючи $ годинник, де потрібно.
Капай

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

Починаючи з кутових 1,5, це фокус на розробці, що базується на компонентах. Рекомендований спосіб взаємодії компонентів - через використання властивості "вимагати" та через прив'язку властивостей (введення / виводу).

Для компонента знадобиться інший компонент (наприклад, кореневий компонент) та отримати посилання на його контролер:

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. З контролера джерела виклик notificationService.Method. Я також передаю відповідний об'єкт для збереження, якщо це потрібно.
  4. У рамках методу я зберігаю дані в службі сповіщень та загальноприйнятий спосіб повідомлення.
  5. У контролері призначення я слухаю ($ range.on) для трансляції події та доступу до даних Служби сповіщень.

Оскільки в будь-якій точці Служба сповіщень є однотонною, вона повинна мати можливість надавати постійні дані по всій.

Сподіваюсь, це допомагає


0

Ви можете використовувати вбудований сервіс AngularJS $rootScopeі вводити цю послугу в обидва контролери. Потім ви можете слухати події, запущені на об’єкт $ rootScope.

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


0

Вам слід скористатися Сервісом, оскільки $rootscopeце доступ із усієї Додатку, і це збільшує навантаження, або ви можете користуватися рутними параметрами, якщо ваших даних не більше.


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 та $ широкомовна трансляція. Згідно з нашими знаннями, це найкращий, ефективний та ефективний спосіб.

Спочатку ми викликаємо функцію від одного контролера.

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 замість $ range. Використовуйте відповідний контролер відповідно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.