07 — Как Angular находит селекторы

Цель: развеять главный миф. Точная версия — 03-selector-matching в соседнем vault'е.

Миф, который надо выбросить из головы

«Angular в браузере сканирует DOM, ищет элементы по селекторам и навешивает на них компоненты.»

Это неправда. Так работает jQuery, но не Angular. Поиск по живому DOM (querySelector) случается ровно один раз — на старте, чтобы найти <app-root> (см. главу 03).

Всё остальное Angular знает заранее — с компиляции. Разберём, как.

Аналогия: сборка мебели по инструкции

Когда ты собираешь шкаф из IKEA, ты не ищешь по всей квартире, куда вкрутить винт. В инструкции уже нарисовано: «винт A → отверстие 5». Тебе не надо искать — тебе сказали.

Так и Angular: компилятор, видя твой шаблон и список imports, заранее знает, какой компонент/директива куда встаёт, и записывает это в инструкцию. В рантайме не надо «искать» — надо просто следовать записанному.

Три уровня — где что происходит

Уровень 1 — компиляция (тут вся работа по селекторам)

Компилятор смотрит на imports твоего компонента и составляет список кандидатов — всех директив/компонентов, которые тут могут встретиться. Этот список зашивается в скомпилированный компонент. Сами селекторы (app-foo, [myDir], .cls) он уже разобрал в удобную форму.

Уровень 2 — старт (единственный реальный DOM-поиск)

bootstrapApplication(App) → Angular делает document.querySelector('app-root'), находит место на странице, ставит туда корневой компонент. Один раз. Всё.

Уровень 3 — построение шаблона (сопоставление без DOM)

Когда строится компонент, для каждого элемента шаблона Angular берёт список кандидатов (с уровня 1) и проверяет: подходит ли селектор каждого кандидата к этому элементу. Но проверяет он не живой DOM, а статическое описание узла (TNode из главы 06) — его тег и атрибуты.

И главное: это сопоставление делается один раз на класс шаблона (в первый проход), результат запоминается в бланке TView. Создаёшь 100 одинаковых компонентов — селекторы матчатся один раз, остальные 99 берут готовое.

Как выглядит сопоставление

Допустим, в шаблоне есть <my-button primary>, а среди кандидатов — компонент с селектором my-button[primary]. Angular проверяет по кусочкам:

селектор:  my-button [primary]
элемент:   <my-button primary>
           ─────────  ────────
проверка:  тег совпал? да   →   атрибут primary есть? да   →   МАТЧ ✅

Если хоть один кусочек не сошёлся — кандидат отбрасывается, берётся следующий.

Полезный факт: селектор по атрибуту [primary] срабатывает и на обычном primary="...", и на биндинге [primary]="x", и на событии — потому что Angular сравнивает имена атрибутов из статического описания, а не из DOM.

Что происходит после матча

Нашли подходящую директиву/компонент → Angular:

  1. создаёт её экземпляр (через DI — склад из главы 03);
  2. прокидывает начальные @Input из атрибутов;
  3. навешивает host-биндинги — это (click)/[class]/[attr], объявленные в самой директиве через host: {...};
  4. у компонента дополнительно создаётся свой LView (своя «копия бланка»).

Частые вопросы

«Почему моя директива не подхватилась?» — Скорее всего, ты не добавил её в imports компонента. Тогда её нет в списке кандидатов, и матчить нечего. Это ошибка компиляции/конфигурации, а не «селектор не нашёлся в браузере».

«Можно два компонента на один элемент?» — Нет. Один компонент-хозяин на элемент (плюс сколько угодно обычных директив). Второй компонент — ошибка компиляции.

Короткий итог

  • Селекторы — это работа компилятора, не браузера.
  • Живой querySelector — один раз, на <app-root>.
  • Внутри шаблонов идёт сопоставление кандидатов со статическим описанием узлов, один раз на класс, результат кешируется.

Дальше: как контент создаётся и переезжает динамически → 08 — Динамика и порталы.