Докладно про внутрішню кухню AngularJS

У фреймворку AngularJS є кілька цікавих рішень в коді. Сьогодні ми розглянемо два з них — як працюють області видимості і директиви.

Перше, чого навчають всіх в AngularJS — директиви повинні взаємодіяти з DOM. А найбільше новачка заплутує процес взаємодії між областями видимості, директивами та контролерами. У цій статті ми розглянемо подробиці роботи областей видимості і життєвий цикл Angular-програми.

Якщо в наступній картинці вам щось незрозуміло — ця стаття для вас.

image

(У статті розглядається 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 область видимості пов'язана з елементом і всіма його дочірніми елементами, при цьому елемент не обов'язково прямо пов'язаний з областю видимості. Елементів призначаються області видимості одним з трьох способів.

Перший — якщо область видимості створюється у елемента контролером або директивою.

<nav ng-controller='menuCtrl'>


Другий — якщо у елемента немає області видимості, він успадковує від батька

<nav ng-controller='menuCtrl'>
<a ng-click='navigate()'>Тисни мене!</a> <!-- also <nav>'s scope -->
</nav>


Третій — якщо елемент не є частиною ng-app, то він не належить ні до однієї області видимості

<head>
<h1>Додаток</h1>
</head>
<main ng-app='PonyDeli'>
<nav ng-controller='menuCtrl'>
<a ng-click='navigate()'>Тисни мене!</a>
</nav>
</main>


Щоб зрозуміти, до якої галузі видимості належить елемент, пройдіться по дереву елементів зсередини назовні, використовуючи три правила. Створює нову область видимості? Тоді він пов'язаний саме з нею. Є в нього батько? Перевіряємо батьків. Якщо він не входить в 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: '<button ng-click="eat()">Хочу поїсти трошки {{type}}!</button>',
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.

<ul ng-app='PonyDeli' ng-controller='foodCtrl' food-tracker>
<li food type='type' ng-repeat='type in foodTypes'></li>
</ul>

angular.module('PonyDeli').controller('foodCtrl', function ($scope) {
$scope.foodTypes = ['цибуля', 'огірок', 'горішок'];
});


Працюючий приклад дивіться на CodePen.

Але чи потрібно вам подія, до якої зможе підключитися, що завгодно? Не буде достатньо сервісу? У цьому випадку можна зробити і так. Можна заперечити, що події потрібні, оскільки ви не знаєте заздалегідь, хто ще буде підписуватися на food.order, а значить, використання подій — більш далекоглядно з точки зору розвитку програми. Також можна сказати, що директива відстеження їжі не потрібна, оскільки вона не взаємодіє з DOM, а тільки чекає події, тому її можна замінити на сервіс.

І це вірні зауваження, в даному випадку. Але коли і інших компонентів потрібно буде спілкуватися з food.order, стане зрозумілою необхідність у подіях. У реальному житті події найбільш корисні, коли вам треба з'єднати кілька областей видимості.

У елементів, які знаходяться на одному рівні, зазвичай спілкування один з одним утруднено, і вони зазвичай роблять це через свого батька. В результаті це виливається в броадкастінг з $rootScope, який слухають всі, кому це потрібно:

<body ng-app='PonyDeli'>
<div ng-controller='foodCtrl'>
<ul food-tracker>
<li food type='type' ng-repeat='type in foodTypes'></li>
</ul>
<button ng-click='deliver()'>Я хочу з'їсти!</button>
</div>
<div ng-controller='deliveryCtrl'>
<span ng-show='received'>
Мавпа вже вислана, скоро ви поїдете.
</span>
</div>
</body>

angular.module('PonyDeli').controller('foodCtrl', function ($rootScope) {
$scope.foodTypes = ['цибуля', 'огірок', 'горішок'];
$scope.deliver = function (req) {
$rootScope.$broadcast('delivery.request', req);
};
});

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, але їх виклик потрібно ініціювати, щоб прив'язка даних відпрацювала правильно. Приклад:

<body ng-app='PonyDeli'>
<ul ng-controller='foodCtrl'>
<li ng-bind='prop'></li>
<li ng-bind='dependency'></li>
</ul>
</body>

angular.module('PonyDeli').controller('foodCtrl', function ($scope) {
$scope.prop = 'початкове значення';
$scope.dependency = 'поки нічого!';

$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
чи була область видимості видалена


У другій частині статті ми розглянемо директиви, ізольовані області видимості, трансклюзии, прив'язані функції, компілятори, контролери директив та інше.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.