Статьи

5 ошибок, которые разработчики AngularJS совершают: чрезмерное использование $ broadcast и $ emit

Это третья часть в серии из пяти статей, в которой рассматриваются распространенные ошибки AngularJS. Напомним, что пять основных ошибок, которые я вижу, делают люди:

  1. Сильная зависимость от $ scope (без использования контроллера как) 
  2. Злоупотребляя $ watch
  3. Чрезмерное использование трансляции $ и $ emit
  4. Взлом DOM
  5. Неспособность проверить

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

Чрезмерное использование трансляции $ и $ emit

Одно из преимуществ использования $ scope — это разнообразие доступных функций, например возможность наблюдать за моделью. Я объяснил, почему это не лучшая идея или подход, и в этом посте мы рассмотрим другой набор функций. По сути, концепция событий и возможность настраивать события — это хорошая идея. В AngularJS пользовательские события поддерживаются с помощью функций $ emit (всплывающее вверх) и $ broadcast (замедление) для публикации события.

В Angular области видимости являются иерархическими. Следовательно, с помощью этих функций можно общаться с детьми или слушать детей. Простая функция $ on используется для регистрации или подписки на событие. Давайте возьмем простой пример. Опять же, я использую что-то придуманное, чтобы показать, как это работает, но, надеюсь, вы сможете экстраполировать на более сложные сценарии, такие как списки master / detail или даже страницы с большим количеством информации, для которой требуется какой-то механизм для автоматического обновления при обновлении одной области.

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

HTML5 выглядит так:

<div ng-app="myApp">
    <div ng-controller="genderCtrl">
        <select ng-options="g as g for g in genders"  
                ng-model="selectedGender">         
        </select>
    </div>
    <div ng-controller="babyCtrl">
        It's a {{genderText}}!
    </div>
    <div ng-controller="watchCtrl">
        Changed {{watches}} times.
    </div>
</div>

Контроллер пола выставляет список и обрабатывает текущий выбор. Поскольку другие контроллеры зависят от этого выбора, он следит за изменением и транслирует событие всякий раз, когда оно изменяется. Некоторые из вас могут узнать это старинное устройство, которое мы использовали в прошлом для трансляции музыки.

Бумбокс

Обычный шаблон, который я вижу реализованным, — это широковещательная передача из корневой области, поскольку она гарантированно охватывает все дочерние области.

function GenderController($scope, $rootScope) {

    $scope.genders = genders;

    $scope.selectedGender = genders[0];

    $scope.$watch('selectedGender', function () {

        $rootScope.$broadcast('genderChanged',

            $scope.selectedGender);

    });

}

Контроллер, который выставляет метку, прослушивает событие и соответственно обновляет текст.

function BabyController($scope) {
    $scope.genderText = labels[0];
    $scope.$on('genderChanged',
        function (evt, newGender) {

        $scope.genderText =
            newGender ===
            genders[0] ? labels[0] : labels[1];
    });
}

Наконец, третий контроллер прослушивает событие и обновляет счетчик при каждом его срабатывании.

function WatchController($scope) {
    $scope.watches = 0;
    $scope.$on('genderChanged', function () {
        $scope.watches += 1;
    });
}

Это приложение работает ( проверьте это здесь ), но я думаю, что мы можем сделать лучше. Почему мне не нравится этот подход? Вот несколько причин:

  • Это создает зависимость от $ scope, в которой я не уверен, что это необходимо.
  • Кроме того, он создает зависимость от $ rootScope.
  • Если я решу не использовать $ rootScope, мои контроллеры должны понимать иерархию $ scope и иметь возможность соответственно $ emit или $ broadcast. Попробуйте это проверить! 
  • Чтобы отреагировать на изменение, также требуется зависимость от $ scope.
  • Теперь я должен понять, как работают пользовательские события, и правильно использовать их соглашение (обратите внимание, например, что новое значение находится во втором параметре, а не в первом). 
  • Сейчас я делаю что-то «угловым путем», что я мог бы сделать с чистым JavaScript. Я уверен, что чем ближе к JavaScript я останусь, тем проще будет обновить и перенести этот код позже.

Хорошо, так каков ответ? Один из способов взглянуть на это не в виде последовательности событий (т.е. пользователь меняет пол, который вызывает изменение, которое запускает событие, которое вызывает ответ), а вместо этого смотрит на результат. Что на самом деле происходит? Смена пола действительно меняет состояние модели. Состояние метки просто чередуется в зависимости от состояния выбора пола, а счетчик повторяется. Таким образом, изменение состояния приводит к изменению модели. Мне действительно нужно сообщение, чтобы сообщить об этом?

Позвольте мне разобрать это по-другому. Во-первых, когда я думаю о чем-то общем между контроллерами, я сразу думаю о сервисе . Определяется ли это через фабрику или службу, здесь не главное (если вы не знаете или не понимаете разницы, прочитайте раздел «Общие сведения о поставщиках, службах и фабриках в Angular» ), а скорее о том, что существует общее состояние, общее для контроллеров. Итак, я создаю гендерный сервис:

function GenderService() { }

angular.extend(GenderService.prototype, {
    getGenders: function () {
        return genders.slice(0);
    },
    getLabelForGender: function () {
        return this.selectedGender === genders[0] ?
            labels[0] : labels[1];
    },
    selectedGender: genders[0]
});

Служба предоставляет мне копию списка полов, позволяет мне получить или установить выбранный пол и возвращает метку для пола. Все полезные, общие функции упакованы в один компонент. Помимо способа, которым я подключил его с помощью функции расширения Angular, это чистый объект POJO, который я могу тестировать без зависимости от Angular.

Теперь я могу создать контроллер, который также является POJO. Он полагается на гендерный сервис, поэтому будет использовать инжектор конструктора. Хотя Angular будет обрабатывать это для меня в приложении, я могу легко создать свой собственный экземпляр службы гендерной проверки или смоделировать его и передать его в конструктор для тестирования, и все равно он не будет зависеть от Angular вообще… или $ scope… или $ emit… или $ трансляция.

function GenderController(genderService) {
    this.genders = genderService.getGenders();
    this.genderService = genderService;
}

Object.defineProperty(GenderController.prototype,
    'selectedGender', {
        enumerable: true,
        configurable: false,
        get: function () {
            return this.genderService.selectedGender;
        },
        set: function (val) {
            this.genderService.selectedGender = val;
        }
    });

Обратите внимание, что наш «контроллер» — это на самом деле просто объект, который предоставляет список полов и обладает свойством, которое передает выбранный пол через гендерную службу. На самом деле я мог бы просто разоблачить сервис и связать его напрямую, но это, на мой взгляд, слишком много информации. Мой пользовательский интерфейс должен касаться только списка и выбора, а не базовой реализации того, как им управляют. Конечным результатом теперь является то, что вы можете выбрать пол, и служба будет удерживать выбор. Заметьте, что он не отправляет никаких сообщений, так как меняется контроллер меток? Взглянуть:

function BabyController(genderService) {
    this.genderService = genderService;
}

Object.defineProperty(BabyController.prototype,
    'genderText', {
        enumerable: true,
        configurable: false,
        get: function () {
            return this.genderService.getLabelForGender();
        }
    });

Видите, как это просто? Я могу написать тест, который создает три POJO (сервис и контроллеры), имитируя обновление выбора на контроллере выбора и проверяя изменения метки пола на контроллере меток. Мой пользовательский интерфейс просто привязывается к интересующему его свойству (свойству гендерного текста) и не путается с какой-либо презентацией или бизнес-логикой, происходящей «за кадром». При обращении к свойству label он запрашивает у службы, какой должна быть метка, и всегда возвращает правильное значение на основе текущего выбора.

К настоящему времени вы, наверное, уже догадались, как выглядит контроллер наблюдателя …

function WatchController(genderService) {

    this.genderService = genderService;

    this._watches = 0;

    this._lastSelection = genderService.selectedGender;

}

Object.defineProperty(

    WatchController.prototype,

    'watches', {

        enumerable: true,

        configurable: false,

        get: function () {

            if (this.genderService.selectedGender !==

                this._lastSelection) {

                this._watches += 1;

                this._lastSelection =

                    this.genderService.selectedGender;

            }

            return this._watches;

        }

    });

Он просто отслеживает часы внутри, и всякий раз, когда к ним обращаются извне, проверяется, изменился ли выбор. Если вам не нравится этот тип мутации в свойстве getter, вы можете вместо этого выставить его как метод и привязать данные к результату вызова метода.

Теперь у меня есть четыре POJO без угловых зависимостей. Я могу проверить их по своему усмотрению, мне не нужно увеличивать объем $, и мне даже не важно, как реализованы $ emit или $ broadcast. Посмотрите на рабочий пример без $ emit или $ broadcast .

Есть одна вещь, которую нужно вызвать. Некоторые из вас могут признать , что этот подход ( в частности , для контроллера часов) это зависит от углового косвенно . Реализация наблюдателя основана на доступе к количеству часов. Независимо от Angular, вы можете манипулировать выделением несколько раз, и наблюдатель пропустит его, если вы не будете опрашивать его каждый раз. Я знаю, что в Angular он всегда будет обновляться из-за цикла дайджеста, поэтому в контексте Angular он будет работать нормально, но если бы я хотел что-то более «независимое», мне нужно было бы выставить событие из службы пола и зарегистрироваться от наблюдателя. сервис для обновления счетчика, даже если он не опрошен. В этом случае я бы, вероятно, реализовал свой собственный агрегатор событий это не зависит от иерархии $ scope или $ scope для работы.

Кстати, причина, по которой это работает без часов или сообщений, заключается в том, как работает цикл Angular $ digest. Когда вы изменяете модель путем изменения пола, Angular автоматически проверяет значения других свойств, связанных с данными. Это, конечно, вызывает получатель, который был реализован, и приводит к тому, что правильное значение возвращается и обновляется в пользовательском интерфейсе. Еще раз, мы избегаем лишних часов.

Поскольку часы и обработчики событий могут изменять модель, Angular на самом деле должен возвращаться и переоценивать выражения каждый раз, когда вызывается функция наблюдения или обработчик событий, чтобы убедиться в отсутствии дальнейших обновлений. В этой модели сама модель уже обновлена, поэтому требуется только один проход (вы увидите несколько проходов, потому что есть несколько привязок, но никаких дополнительных проходов из-за часов).

Такой подход уменьшает зависимости и, следовательно, сложность кода, облегчает тестирование и обеспечивает повышение производительности. Это на самом деле просто включает в себя размышление о вещах с точки зрения отношений и состояния, а не идею «процесса», который «толкает» вещи. Пусть Angular справится со всем этим подъемом и сделает ваш дизайн простым и понятным. В следующем посте я расскажу о более сложном проекте, в котором показано полностью работающее приложение, использующее этот принцип без каких-либо зависимостей от $ scope внутри контроллеров.

Хотя я рассмотрел некоторые распространенные сценарии, связанные с привязкой данных, вы всегда будете сталкиваться с ситуациями, требующими манипулирования DOM. Это может быть что-то простое, например, обновление заголовка страницы на основе изменения контекста, или что-то более сложное, например рендеринг SVG, или установка фокуса на элемент управления на основе ошибки проверки. Это часто приводит к четвертой ошибке, которую, как я вижу, делают разработчики, которая заключается в прямом взломе DOM, часто с самого контроллера!

В следующем посте я поделюсь с вами, как справиться с этим с помощью AngularJS. Пожалуйста, уделите некоторое время, чтобы прокомментировать и поделиться своими мыслями со мной, и если вы считаете, что я могу предоставить дополнительную ценность, пожалуйста, не стесняйтесь — контактная форма на этой странице отправит мне электронное письмо напрямую, так что дайте мне знать, как я могу помочь !

Спасибо,

Подпись [1]