[Перевод] Подробно о внутренней кухне AngularJS
У фреймворка AngularJS есть несколько интересных решений в коде. Сегодня мы рассмотрим два из них — как работают области видимости и директивы.Первое, чему обучают всех в AngularJS — директивы должны взаимодействовать с DOM. А больше всего новичка запутывает процесс взаимодействия между областями видимости, директивами и контроллерами. В этой статье мы рассмотрим подробности работы областей видимости и жизненный цикл Angular-приложения.
Если в следующей картинке вам что-то непонятно — эта статья для вас.
(В статье рассматривается AngularJS 1.3.0)AngularJS использует области видимости, чтобы абстрагировать общение директив и DOM. Области видимости есть и на уровне контроллеров. Области видимости — это простые объекты JavaScript (plain old JavaScript objects, POJO). Они добавляют кучку «внутренних» свойств, которые предваряются одним или двумя символами $. Те, у которых стоит префикс $$, не нужно использовать в коде слишком часто — обычно их использование говорит о непонимании работы приложения.
Так что это за области видимости такие? На жаргоне AngularJS область видимости означает не то, что под этим подразумевается в коде JS. Обычно под областью видимости понимают блок кода, которая содержит контекст, разные переменные и т.п. К примеру: function eat (thing) { console.log ('Кушаю ' + thing); }
function nuts (peanut) { var hazelnut = 'hazelnut'; // фундук
function seeds () { var almond = 'almond'; // миндаль eat (hazelnut); // Отсюда я могу залезть в мешок! }
// Миндаль здесь недоступен. } Однако это не те области видимости, про которые идёт речь в AngularJS.
Наследование областей видимости в AngularJS Область видимости в AngularJS также является контекстом, но только в понимании AngularJS. В AngularJS область видимости связана с элементом и всеми его дочерними элементами, при этом элемент не обязательно прямо связан с областью видимости. Элементам назначаются области видимости одним из трёх способов.Первый — если область видимости создаётся у элемента контроллером или директивой.
Третий — если элемент не является частью ng-app, то он не принадлежит ни к одной области видимости
Приложение
Вызываем внутренние свойства областей видимости AngularJS
Пройдёмся по некоторым типичным свойствам. Для этого я открою Chrome и перейду к приложению, над которым работаю. Затем я открою Developer tools для просмотра свойств элемента. Знаете ли вы, что $0 даёт доступ к последнему выбранному элементу в панели «Elements»? $1 — предыдущий выбранный элемент, и т.д. $0 мы будем использовать чаще всего.
angular.element обёртывает каждый элемент DOM либо в jQuery, либо в jqLite. После этого у вас появляется доступ к функции scope (), возвращающей область видимости элемента. Скомбинируем это с $0 и получим часто используемую команду:
angular.element ($0).scope () Раз уж используется jQuery, то $($0).scope () тоже сработает. Теперь посмотрим, какие же свойства доступны в типичной области видимости — те, которые записываются начиная с $.
for (o in $($0).scope ())o[0]=='$'&&console.log (o)
Изучаем внутренности области видимости AngularJS
Перечислю свойства, которые вывела эта команда, сгруппировав их по функциональности. Начнём с простых.
$id идентификатор области видимости $root корневая область видимости $parent родительская область видимости, или null, если scope == scope.$root $$childHead область видимости первого дочернего узла, или null $$childTail область видимости последнего дочернего узла, или null $$prevSibling область видимости предыдущего узла этого же уровня, или null $$nextSibling область видимости следующего узла этого же уровня, или null Навигация при помощи этих свойств — неудобная. Может иногда пригодиться $parent, но всегда есть более удобные способы обращаться к родительским элементам. Например, использование событий, которые мы рассмотрим в следующей части списка.
Событийная модель в области видимости AngularJS
Следующие свойства помогают определять события и подписываться на них.
$$listeners обработчики событий, зарегистрированные в области видимости $on (evt, fn) присоединяет обработчик fn на событие evt $emit (evt, args) запускает событие evt, проходящее вверх по цепочке областей видимости, начиная с текущего и заканчивая всеми родительскими $parent, включая $rootScope $broadcast (evt, args) запускает событие evt, проходящее вниз по цепочке областей видимости, начиная с текущего и заканчивая всеми дочерними При запуске, обработчики событий получают объект события и любые аргументы, переданные в $emit или $broadcast. Как же можно использовать события?
Директива может использовать их для сообщения о важном событии. В примере событие запускается по нажатию кнопки.
angular.module ('PonyDeli').directive ('food', function () { return { scope: { // К области видимости директив я ещё вернусь type: '=type' }, template: '', link: function (scope, element, attrs) { scope.eat = function () { letThemHaveIt (); scope.$emit ('food.order, scope.type, element); };
function letThemHaveIt () { // Возня с UI } } }; }); Я задаю событиям пространства имён. Это предотвращает пересечение имён, и помогает понять, откуда приходят события или на какое событие вы подписываетесь. Предположим, вас интересует аналитика и вы хотите отследить все нажатия food через Mixpanel. Вместо замусоривания контроллера или директивы, вы можете сделать отдельную директиву для отслеживания нажатий, которая будет отдельной вещью в себе.
angular.module ('PonyDeli').directive ('foodTracker', function (mixpanelService) { return { link: function (scope, element, attrs) { scope.$on ('food.order, function (e, type) { mixpanelService.track ('food-eater', type); }); } }; }); Реализация сервиса здесь не важна — она просто служила бы обёрткой клиентского API от Mixpanel. HTML выглядел бы так, как указано ниже, и я бы добавил ещё контроллер, содержащий все нужные типы еды. Для завершения примера я добавлю ng-repeat, чтобы можно было выводить списки еды, не копируя код. Просто выведем их циклом по foodTypes, который доступен в области видимости foodCtrl.
angular.module ('PonyDeli').controller ('foodCtrl', function ($scope) { $scope.foodTypes = ['лучок', 'огурчик', 'орешек']; }); Работающий пример смотрите на CodePen.
Но нужно ли вам событие, к которому сможет подключиться что угодно? Не будет ли достаточно сервиса? В этом случае можно сделать и так. Можно возразить, что события нужны, поскольку вы не знаете заранее, кто ещё будет подписываться на food.order, а значит, использование событий — более дальновидно с точки зрения развития приложения. Также можно сказать, что директива отслеживания еды не нужна, поскольку она не взаимодействует с DOM, а только ждёт события, поэтому её можно заменить на сервис.
И это верные замечания, в данном случае. Но когда и другим компонентам нужно будет общаться с food.order, станет ясна необходимость в событиях. В реальной жизни события наиболее полезны, когда вам надо соединить несколько областей видимости.
У элементов, находящихся на одном уровне, обычно общение друг с другом затруднено, и они обычно делают это через своего родителя. В результате это выливается в броадкастинг из $rootScope, который слушают все, кому это нужно:
angular.module ('PonyDeli').controller ('deliveryCtrl', function ($scope) { $scope.$on ('delivery.request', function (e, req) { $scope.received = true; // обработка запроса }); }); Также можно посмотреть работу на CodePen.
Я бы сказал, что события нужно использовать, когда вы ожидаете изменения Вида в ответ на событие, а сервисы — когда Виды не меняются.
Если у вас две компоненты общаются через $rootScope, то лучше использовать $rootScope.$emit и $rootScope.$on вместо $broadcast. Тогда событие распространяется только среди $rootScope.$$listeners, и не будет терять время на проход всех дочерних узлов $rootScope, у которых нет обработчиков этого события. В примере сервис использует $rootScope для событий, не ограничиваясь определённой областью видимости. Он предоставляет метод subscribe для подписки на прослушивание событий.
angular.module ('PonyDeli').factory («notificationService», function ($rootScope) { function notify (data) { $rootScope.$emit («notificationService.update», data); }
function listen (fn) { $rootScope.$on («notificationService.update», function (e, data) { fn (data); }); }
// Всё, у чего есть смысл для создания событий в будущем function load () { setInterval (notify.bind (null, 'Что-то случилось!'), 1000); }
return { subscribe: listen, load: load }; }); И это тоже есть на CodePen.
Digest Привязка к данным у AngularJS работает посредством цикла, который отслеживает изменения и запускает события. В цикле $digest есть несколько методов. По-первых, это scope.$digest, рекурсивно переваривающий изменения в текущей области видимости и дочерних областях. $digest () исполняет цикл $$phase текущая фаза цикла — один из вариантов [null, '$apply', '$digest'] Не стоит запускать digest, если вы уже находитесь в фазе digest — это приведёт к непредсказуемым последствиям. Что говорится по поводу digest в документации:
Запускает всех наблюдателей (watcher) в текущей области видимости и её дочерних областях. Поскольку слушатель (listener) наблюдателя может менять модель, $digest () вызывает наблюдателей до тех пор, пока их слушатели не перестанут выполняться. Это может привести к попаданию в бесконечный цикл. Поэтому функция выбросит ошибку 'Достигнуто максимальное количество итераций', если их количество превысит 10.
Обычно $digest () не вызывается напрямую из контроллеров или директив. Нужно вызывать $apply () (обычно это делают изнутри директив), который сам уже вызовет $digest ().
Значит, $digest обрабатывает всех наблюдателей, и затем всех тех наблюдателей, которые вызываются предыдущими наблюдателями, до тех пор, пока они не перестанут выполняться. Остаётся два вопроса:
— кто такие наблюдатели? — что вызывает $digest?
Возможно, вы уже знаете, что такое «наблюдатель» и использовали scope.$watch, а может даже и scope.$watchCollection. Свойство $$watchers содержит всех наблюдателей из области видимости.
$watch (watchExp, listener, objectEquality) добавляет слушателя в область видимости $watchCollection наблюдает за элементами массива или свойствами объекта $$watchers содержит всех наблюдателей из области видимости Наблюдатели — самый важный аспект AngularJS, но их вызов нужно инициировать, чтобы привязка данных отработала правильно. Пример:
$scope.$watch ('prop', function (value) { $scope.dependency = 'prop содержит »' + value + '»! ну ничего ж себе'; });
setTimeout (function () { $scope.prop = 'другое значение'; }, 1000); }); Значит, у нас есть 'начальное значение', и мы ожидаем, что вторая строка HTML поменяется на 'prop содержит «другое значение»! ну ничего ж себе', не так ли? И можно было бы ожидать, что первая строка поменяется на 'другое значение'. Почему она не меняется?
Многое из того, что вы создаёте в HTML, в результате создаёт наблюдателя. В нашем случае, каждая директива ng-bind создаёт наблюдателя для свойства. Она обновляет в HTML, когда prop и dependency меняются. Поэтому, в нашем коде есть три наблюдателя — по одному на каждый ng-bind, и один для контроллера. Откуда AngularJS узнает, что свойство обновилось после таймаута? Можно напомнить ему об этом, добавив вызов digest на обратный вызов таймера:
setTimeout (function () { $scope.prop = 'другое значение'; $scope.$digest (); }, 1000); Я сохранил два примера на CodePen — один без $digest, а второй — с ним. Но более правильный способ — использовать сервис $timeout вместо setTimeout. Он даёт возможность обработки ошибок и выполняет $apply ().
$timeout (function () { $scope.prop = 'другое значение'; }, 1000); $apply (expr) разбирает и вычисляет выражение, и выполняет цикл $digest по $rootScope Теперь по поводу того, кто вызывает $digest. Эти функции вызываются самим AngularJS в стратегических местах кода. Их можно вызвать напрямую, или через вызов $apply (). Большинство директив фреймворка вызывают эти функции. Они вызывают наблюдателей, а наблюдатели обновляют интефейс.
Посмотрим на список свойств, связанных с циклом $digest, которые можно обнаружить в области видимости.
$eval (expression, locals) разбор и немедленное выполнение выражения $evalAsync (expression) разбор и отложенное выполнение выражения $$asyncQueue асинхронная очередь задач, обрабатывается на каждом цикле digest $$postDigest (fn) выполняет fn после следующего цикла digest $$postDigestQueue зарегистрированные при помощи $$postDigest (fn) методы Вот ещё несколько свойств области видимости, имеющие дело с её жизненным циклом и обычно используемые для внутренних целей. Но в некоторых случаях может потребоваться создание новых областей видимости через $new.
$$isolateBindings изоляция привязок области видимости (к примеру, { options: '@megaOptions' } $new (isolate) создаёт дочернюю область видимости или изолированную область, которая не будет наследником родителя $destroy удаляет область видимости из цепочки областей. её дочерние области не будут получать информацию о событиях и их наблюдатели не будут выполняться $$destroyed была ли область видимости удалена Во второй части статьи мы рассмотрим директивы, изолированные области видимости, трансклюзии, привязанные функции, компиляторы, контроллеры директив и другое.