AngularInDepth уходит от Medium. Эта статья, ее обновления и более свежие статьи размещены на новой платформе inDepth.dev

Компонент - это просто директива с шаблоном? Либо это?

С того момента, как я начал использовать Angular, меня заинтриговал вопрос, в чем разница между компонентами и директивами. Это назойливый вопрос, особенно для тех, кто пришел из мира AngularJS, поскольку у нас были только директивы, которые мы часто использовали в качестве компонентов. Если вы поищете в Интернете объяснение, вы увидите много таких фраз:

Компоненты - это просто директивы с содержанием, определенным в шаблоне…

Компоненты Angular - это подмножество директив. В отличие от директив, в компонентах всегда есть…

Компоненты - это директивы высшего порядка с шаблонами, которые служат…

Утверждения кажутся правдивыми, поскольку, когда я посмотрел на фабрики, созданные для компонентов, я не нашел там определений компонентов! И вы тоже не найдете. Только директивы…

И я не нашел объяснения почему, потому что для его предоставления нужно хорошо понимать, как работает Angular внутри. Если этот вопрос какое-то время вас беспокоил, то эта статья для вас. Он призван раскрыть тайну. Но будьте готовы к хардкорным вещам 😎.

По сути в этой статье объясняется, как Angular представляет компоненты и директивы под капотом, и вводит новое определение узла представления - определение директивы.

Я работаю адвокатом разработчиков в ag-Grid. Если вам интересно узнать о сетках данных или вы ищете идеальное решение для сетки данных Angular, попробуйте его с помощью руководства « Начать работу с сеткой Angular за 5 минут ». Я с радостью отвечу на любые ваши вопросы. И следите за мной, чтобы оставаться в курсе!

Старый добрый -f оптимистичный взгляд

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

Итак, если у вас есть такой шаблон:

<div><h1>Hello {{name}}</h1></div>

компилятор сгенерирует определение представления с двумя узлами элементов для div и h1 элементов DOM и одним текстовым узлом для части Hello {{name}}. Это очень важные узлы, поскольку без них мы не смогли бы ничего увидеть на экране. Но поскольку шаблон композиции компонентов диктует, что мы должны иметь возможность вкладывать компоненты, должен быть другой тип узла представления для встроенных компонентов. Чтобы выяснить, что это за специальные узлы, давайте сначала посмотрим, из чего состоит компонент. Компонент - это, по сути, элемент DOM с присоединенным поведением, реализованным в классе компонента. Начнем с элемента DOM.

Пользовательские элементы DOM

Вы, наверное, знаете, что можете создать новый тег HTML и использовать его в своем HTML. Например, если вы не используете какой-либо фреймворк и вставляете в свой html следующее:

<a-comp></a-comp>

а затем запросите узел DOM и проверьте его тип, вы увидите, что это совершенно допустимый элемент DOM:

const element = document.querySelector('a-comp');
element.nodeType === Node.ELEMENT_NODE; // true

Этот a-comp элемент будет создан браузером с использованием интерфейса HTMLUnknownElement, который наследуется от интерфейса HTMLElement, но без реализации каких-либо дополнительных свойств или методов. Вы сможете стилизовать его с помощью CSS, а также прикрепить прослушиватели событий к общим событиям, таким как click. Как я уже сказал, отлично подходит элемент html.

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

class AComponent extends HTMLElement {...}
window.customElements.define('a-comp', AComponent);

Похоже ли это на то, чем вы занимаетесь какое-то время?

Да, это очень похоже на то, что мы делаем в Angular при определении компонента. На самом деле Angular довольно точно следует спецификации веб-компонентов, но упрощает для нас многие вещи, поэтому нам не нужно самостоятельно создавать теневой корень и прикреплять его к элементу хоста. Однако компоненты, которые мы создаем в Angular, не регистрируются как пользовательские элементы и обрабатываются фреймворком очень специфическим образом. Если вам интересно, как создать компонент без какой-либо инфраструктуры, прочтите Custom Elements v1: Reusable Web Components.

Итак, мы увидели, что можем создать любой HTML-тег и использовать его в шаблоне. Неудивительно, что если мы используем его в шаблоне компонента Angular, фреймворк создаст определение элемента для этого тега:

function View_AppComponent_0(_l) {
    return jit_viewDef2(0, [
        jit_elementDef3(0, null, null, 1, 'a-comp', [], ...)
    ])
}

Однако вам необходимо указать Angular, что вы используете настраиваемый элемент, добавив schemas: [CUSTOM_ELEMENTS_SCHEMA] к свойствам модуля или декоратора компонента, иначе компилятор Angular выдаст ошибку:

'a-comp' is not a known element:
1. If 'a-comp' is an Angular component, then ...
2. If 'a-comp' is a Web Component then add...

Итак, у нас есть элемент, но нам не хватает класса. Есть ли что-нибудь в Angular, у которого есть класс, кроме компонента? Конечно, есть - директива! Давайте добавим директиву и посмотрим, что у нас получится.

Определение директивы

Вы, наверное, знаете, что у каждой директивы есть селектор, который можно использовать для нацеливания на определенный элемент DOM. В большинстве директив используются селекторы атрибутов, но с селекторами элементов тоже все в порядке. Фактически, директива формы Angular использует селектор элементов form для неявного присоединения определенного поведения к html-формам.

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

@Directive({selector: 'a-comp'})
export class ADirective {}

А теперь проверим завод:

function View_AppComponent_0(_l) {
    return jit_viewDef2(0, [
        jit_elementDef3(0, null, null, 1, 'a-comp', [], ...),
        jit_directiveDef4(16384, null, 0, jit_ADirective5, [],...)
    ], null, null);
}

Хорошо, теперь компилятор добавил новый узел jit_directiveDef4 в определение представления вместе с определением элемента. Он также устанавливает параметр childCount для определения элемента в 1, потому что все директивы, применяемые к элементу, считаются дочерними для этого элемента.

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

+----------------+-------------------------------------------+
|      Name      |                Description                |
+----------------+-------------------------------------------+
| matchedQueries | used when querying child nodes            |
| childCount     | specifies how many children               |
|                | the current element have                  |
| ctor           | reference to the component or             |
|                | directive constructor                     |
| deps           | an array of constructor dependencies      |
| props          | an array of input property bindings       |
| outputs        | an array of output property bindings      |
+----------------+-------------------------------------------+

В данной статье нас интересует только параметр ctor. Это просто ссылка на класс ADirective, который мы определили для директивы. Когда Angular будет создавать экземпляры директив (я скоро напишу об этом, поэтому не забудьте подписаться на меня 😃), он создаст экземпляр класса директивы здесь. Затем он сохранит их как данные поставщика на узле просмотра.

Итак, наши эксперименты показывают, что компонент - это просто определение элемента и директивы. Это просто так? Как вы, наверное, знаете, с Angular всегда не все так просто.

Представление компонента

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

@Component({
  selector: 'a-comp',
  template: '<span>I am A component</span>'
})
export class AComponent {}

Готовы сравнить сейчас? Вот сгенерированная фабрика:

function View_AppComponent_0() {
    return jit_viewDef2(0, [
        jit_elementDef3(0, null, null, 1, 'a-comp', [], ...
                    jit_View_AComponent_04, jit__object_Object_5),
        jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)

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

Флаги узлов

Флаги узлов - это первый параметр для всех определений узлов. Фактически это битовая маска флагов узлов, которые содержат конкретную информацию об узлах, используемую фреймворком в основном во время цикла обнаружения изменений. И это число в обоих случаях разное: 16384 - для простой директивы и 49152 для директивы компонента. Чтобы понять, какие флаги были установлены компилятором, давайте просто преобразуем числа в двоичную форму:

16384 =  100000000000000 // 15th bit set
49152 = 1100000000000000 // 15th and 16th bit set

Если вам интересно, как происходит преобразование, прочтите Простая математика, лежащая в основе алгоритмов десятично-двоичного преобразования. Итак, для простой директивы компилятор устанавливает только бит 15-th, что делается в исходных кодах Angular следующим образом:

TypeDirective = 1 << 14

а для узла компонента установлены оба бита 15-th и 16-th, что является

TypeDirective = 1 << 14
Component = 1 << 15

Теперь должно быть понятно, почему цифры различаются. Узел, сгенерированный для директивы, помечается как TypeDirective узел, а узел, сгенерированный для директивы компонента, дополнительно помечается как Component.

Просмотр определения определения

Поскольку a-comp теперь является компонентом со следующим простым шаблоном:

<span>I am A component</span>

компилятор создает для него фабрику со своим собственным определением представления и узлами представления:

function View_AComponent_0(_l) {
    return jit_viewDef1(0, [
        jit_elementDef2(0, null, null, 1, 'span', [], ...),
        jit_textDef3(null, ['I am A component'])

Angular - это дерево представлений, поэтому определение родительского представления должно иметь ссылку на определения дочерних представлений. Определения дочерних представлений хранятся в узлах элементов, созданных для компонентов. В нашем случае узел определения элемента, созданный для a-comp, будет содержать представление для a-comp. А параметр jit_View_AComponent_04, который получает узел элемента a-comp, является ссылкой на прокси-класс, который разрешит фабрику, которая создаст определение представления. Каждое определение представления создается только один раз и затем сохраняется в DEFINITION_CACHE. Это определение представления затем используется, когда Angular создает экземпляр представления.

Тип средства визуализации компонентов

Angular использует несколько рендеров DOM в зависимости от режима ViewEncapsulation, указанного в декораторе компонента:

Рендерер для компонента создается классом DomRendererFactory2. Параметр componentRendererType, который передается внутри определения (в нашем случае это jit__object_Object_5), по сути, является дескриптором средства визуализации, которое необходимо создать для компонента. Самая важная информация, которую он содержит, - это режим инкапсуляции представления и стили, которые необходимо применить к представлению компонента:

{
  styles:[["h1[_ngcontent-%COMP%] {color: green}"]], 
  encapsulation:0
}

Если вы определяете какие-либо стили для своего компонента, компилятор автоматически устанавливает для вашего компонента режим инкапсуляции ViewEncapsulation.Emulated. Или вы можете явно указать режим с помощью свойства декоратора компонента encapsulation. Если вы не устанавливаете никаких стилей и не указываете режим инкапсуляции для вашего компонента, дескриптор определяется как ViewEncapsulation.Emulated и фактически игнорируется. Компонент с таким дескриптором будет использовать средство визуализации родительского компонента.

Дочерние директивы

Остается последняя часть информации - это то, что будет сгенерировано, если мы применим директиву к компоненту в шаблоне следующим образом:

<a-comp adir></a-comp>

Мы уже знаем, что при создании фабрики для AComponent компилятор создаст определение элемента для a-comp HTML-элемента и определение директивы для класса AComponent. Но поскольку компилятор генерирует узел определения директивы для каждой директивы, фабрика для вышеуказанного шаблона будет выглядеть так:

function View_AppComponent_0() {
    return jit_viewDef2(0, [
        jit_elementDef3(0, null, null, 2, 'a-comp', [], ...
    jit_View_AComponent_04, jit__object_Object_5),

    jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)
    jit_directiveDef6(16384, null, 0, jit_ADirective8, [], ...)

И в нем нет ничего, чего мы раньше не видели. Было добавлено еще одно определение директивы, и количество дочерних элементов для элемента было увеличено до 2.

Вот и все. Уф!

Спасибо за прочтение! Если вам понравилась эта статья, нажмите кнопку хлопка под 👏. Это очень много значит для меня и помогает другим людям увидеть историю. Чтобы узнать больше, подпишитесь на меня в Twitter и Medium.