Как Angular находит селекторы и привязывает директивы/компоненты
Версия 21.0.3, Ivy. Главный вопрос: «как Angular в рантайме ищет селекторы и
подвязывается к ним». Сразу снимаем путаницу: бОльшая часть матчинга — на этапе
компиляции, в рантайме «поиск селектора» в реальном DOM есть лишь в одной точке
(bootstrap), а внутри шаблонов идёт сопоставление кандидатов из заранее зашитого списка
против TNode.attrs — без обращения к DOM. Структуры (TView/LView/TNode) —
02-view-data-structures.
TL;DR — три уровня «нахождения»
- Compile-time: компилятор знает
imports/dependenciesкомпонента и зашивает в егоdefineComponentсписок директив-кандидатов (directiveDefs). Сам по элементу он директивы НЕ резолвит в рантайм-DOM — он лишь готовит реестр и template-функцию с инструкциямиɵɵdomElement(...). - Runtime, bootstrap корня: ЕДИНСТВЕННЫЙ настоящий
querySelectorпо живому DOM — найти host-элемент корневого компонента (<app-root>). - Runtime, create-проход шаблона: для каждого элемента в первый проход
(
firstCreatePass) Angular перебирает реестр директив-кандидатов и тестирует селектор каждого противTNode.attrs(НЕ против DOM). Совпавшие — инстанцирует, вешает host-биндинги. Результат кешируется вTView, при повторных инстансах матчинг не повторяется.
Уровень 1 — что зашивает компилятор
ɵɵdefineComponent (render3/definition.ts:120) сохраняет:
selectors: CssSelectorList— селекторы самого компонента (по ним его найдут как кандидата В РОДИТЕЛЬСКОМ шаблоне);dependencies→ из них при созданииTViewстроитсяdirectiveRegistry(render3/view/construction.ts:112) — плоский список всех директив/компонентов, видимых в этом шаблоне.
CssSelectorList — не строка, а уже распарсенный компилятором массив с флагами
(SelectorFlags.ELEMENT/ATTRIBUTE/CLASS/NOT). Т.е. парсинг CSS-селектора сделан в
compile-time; рантайму остаётся дешёвое сравнение по флагам.
Уровень 2 — bootstrap корня (единственный DOM-query)
Цепочка: bootstrapApplication → ApplicationRef.bootstrap → ComponentFactory.create
(render3/component_ref.ts:254). Внутри:
const hostElement = rootSelectorOrNode
? locateHostElement(hostRenderer, rootSelectorOrNode, cmpDef.encapsulation, injector)
: createHostElement(cmpDef, hostRenderer); // синтетический, если узел не задан
locateHostElement (render3/instructions/shared.ts:184) дёргает
renderer.selectRootElement(selector) — вот он, реальный document.querySelector по
селектору корневого компонента. Дальше на найденном узле сразу запускается тот же
create-проход, что и для любого элемента (см. Уровень 3): directiveHostFirstCreatePass
→ createDirectivesInstances → renderView.
Вывод: «поиск селектора в живом DOM» в Angular = только эта точка. Всё остальное —
сопоставление со статическими TNode.attrs.
Уровень 3 — create-проход: матчинг внутри шаблона
Инструкция элемента (в v21 — ɵɵdomElementStart/ɵɵelementStart,
render3/instructions/element.ts:78):
const tNode = tView.firstCreatePass
? directiveHostFirstCreatePass(adjustedIndex, lView, TNodeType.Element, name,
findDirectiveDefMatches, ...) // <-- матчинг ТОЛЬКО в первый проход
: (tView.data[adjustedIndex] as TElementNode); // <-- потом берём готовый TNode
...
if (isDirectiveHost(tNode)) {
createDirectivesInstances(tView, lView, tNode); // <-- инстанцируем найденное
executeContentQueries(tView, tNode, lView);
}
Два ключевых момента: (1) матчинг идёт один раз на класс шаблона; результат
(directiveStart/directiveEnd, флаги) пишется в TNode и переиспользуется. (2) Матчинг
смотрит в TNode.attrs, а не в DOM-атрибуты.
3.1 Перебор кандидатов: findDirectiveDefMatches
render3/instructions/shared.ts:459:
const registry = tView.directiveRegistry; // кандидаты из dependencies
for (let i = 0; i < registry.length; i++) {
const def = registry[i];
if (isNodeMatchingSelectorList(tNode, def.selectors!, /*isProjectionMode*/ false)) {
matches ??= [];
if (isComponentDef(def)) matches.unshift(def); // компонент — в начало
else matches.push(def);
}
}
То есть для каждого элемента линейно проходим список директив шаблона и проверяем селектор каждого. Компонент (если совпал) ставится первым. На элементе допустим максимум один компонент + сколько угодно директив.
3.2 Сам матчинг: isNodeMatchingSelector
render3/node_selector_matcher.ts:113 — сердце темы. Это маленький конечный автомат по
распарсенному селектору с режимами ELEMENT → ATTRIBUTE/CLASS и поддержкой :not:
- режим
ELEMENT: сравнивает имя тега (hasTagAndTypeMatch); для<ng-template>тег подменяется наNG_TEMPLATE_SELECTOR; - режим
ATTRIBUTE: ищет имя атрибута вTNode.attrsчерезfindAttrIndexInNode; если у селектора задано значение — сравнивает (lowercase, без регистра); - режим
CLASS: матчит класс черезisCssClassMatching; :not(...): инвертирует ожидание (SelectorFlags.NOT), при провале —skipToNextSelector.
findAttrIndexInNode (node_selector_matcher.ts:236) знает про плоский формат attrs с
маркерами: пропускает Classes/Styles-секции, переключается в «binding mode» после
AttributeMarker.Bindings/I18n (там только имена, без значений), игнорирует namespaced
и Template-атрибуты при директивном матчинге. Поэтому селектор [foo] совпадает и с
[foo]="x" (binding), и с обычным foo="...".
isNodeMatchingSelectorList (:286) — просто «ИЛИ» по списку: совпал хоть один селектор
из CssSelectorList → матч.
3.3 Инстанцирование: createDirectivesInstances
render3/instructions/shared.ts:123 → instantiateAllDirectives (:386):
if (isComponentHost(tNode))
createComponentLView(lView, tNode, /*componentDef*/ ...); // у компонента — свой LView
...
for (let i = start; i < end; i++) { // start/end = directiveStart/End
const def = tView.data[i] as DirectiveDef;
const directive = getNodeInjectable(lView, tView, i, tNode); // вызов DI-фабрики
attachPatchData(directive, lView); // метка __ngContext__
if (initialInputs !== null)
setInputsFromAttrs(lView, i - start, directive, def, tNode, initialInputs); // статические @Input из attrs
}
Каждая директива конструируется через node-injector (DI), кладётся в свой expando-слот
LView, получает начальные @Input из статических атрибутов. У компонента дополнительно
создаётся отдельный дочерний LView (его собственный шаблон).
3.4 Host-биндинги и host-листенеры
После инстанцирования — invokeDirectivesHostBindings (shared.ts:422), если у узла
стоит TNodeFlags.hasHostBindings. Для каждой директивы с hostBindings:
def.hostBindings!(RenderFlags.Create, directive); // создание: регистрация листенеров и т.п.
В create-режиме host: {'(click)': ...} регистрирует листенер (через ɵɵlistener/
ɵɵdomListener), статические hostAttrs применяются к элементу. Параллельно в первый
проход registerHostBindingOpCodes (render3/view/directives.ts:485) пакует обновляемые
host-биндинги в TView.hostBindingOpCodes — компактную «программу» из чисел:
отрицательное число = «выбери этот элемент» (~tNode.index), дальше тройка
(directiveIdx, varsIdx, hostBindingsFn).
В update-проходе processHostBindingOpCodes (render3/instructions/change_detection.ts:539)
гоняет эту программу: hostBindingFn(RenderFlags.Update, context) — так host-[class]/
[attr]/[style] директив пересчитываются на каждом CD-цикле (увязка с 03-runtime).
Полный поток (create-проход) одной строкой
ɵɵdomElementStart
└─(firstCreatePass)─ directiveHostFirstCreatePass
└─ findDirectiveDefMatches // перебор tView.directiveRegistry
└─ isNodeMatchingSelectorList // тест селектора против TNode.attrs
└─ запись directiveStart/End + flags в TNode
└─ createDirectivesInstances
├─ instantiateAllDirectives // DI-фабрики, @Input из attrs, LView компонента
└─ invokeDirectivesHostBindings // hostBindings(Create): листенеры, hostAttrs
+ registerHostBindingOpCodes (для update)
Частые недопонимания
- «Angular в рантайме сканирует DOM и ищет селекторы» — нет. DOM-query один раз на
корне. Внутри — матч против статических
TNode.attrs, и только в первый проход. [attr]vsattr— селектор по атрибуту матчит и обычный атрибут, и property/event binding (через binding-mode вfindAttrIndexInNode).- Почему директива не подхватилась — её нет в
directiveRegistry, т.е. не попала вimports/dependenciesшаблона (compile-time), а не «селектор не нашёлся в рантайме». - Один компонент на элемент —
findDirectiveDefMatchesставит компонент первым; второй компонент на том же узле — ошибка компиляции.
Привязка к исходникам
render3/definition.ts:120—ɵɵdefineComponent(хранитselectors).render3/view/construction.ts:112—directiveRegistryизdependencies.render3/instructions/shared.ts:184—locateHostElement→selectRootElement(DOM-query);:459—findDirectiveDefMatches;:386—instantiateAllDirectives;:422—invokeDirectivesHostBindings.render3/component_ref.ts:254—ComponentFactory.create(bootstrap корня).render3/instructions/element.ts:78—ɵɵelementStart/create-проход элемента.render3/node_selector_matcher.ts:113—isNodeMatchingSelector;:236—findAttrIndexInNode;:286—isNodeMatchingSelectorList.render3/view/directives.ts:485—registerHostBindingOpCodes;render3/instructions/change_detection.ts:539—processHostBindingOpCodes.