09 — DI на узлах: инжекторы на пальцах

Цель: понять, что такое Dependency Injection в Angular, правда ли «на каждый элемент создаётся свой инжектор» (спойлер: нет), и как Angular находит зависимость, когда ты пишешь inject(SomeService). Точная версия с номерами строк — в соседнем vault'е (notes/v21/runtime/).

Что такое DI вообще (одно предложение)

DI = «не создавай зависимость сам — попроси, и тебе её принесут».

Вместо this.api = new ApiService() ты пишешь api = inject(ApiService) — и Angular сам находит готовый экземпляр и отдаёт тебе. Кто и где его создаёт — забота Angular, не твоя.

Аналогия: ты в отеле. Нужен фен — ты не идёшь в магазин покупать. Ты звонишь на ресепшн: «принесите фен». Тебе принесут уже готовый. inject() — это твой звонок.

Два этажа инжекторов

В Angular есть две независимые иерархии инжекторов:

1. EnvironmentInjector — «здание целиком»
   корневой инжектор приложения (provideHttpClient, provideRouter, root-сервисы)
   общий для всех. Сюда складывают то, что одно на всё приложение.

2. ElementInjector (node injector) — «комната в здании»
   живёт на УЗЛАХ шаблона. Компонент/директива могут раздавать
   зависимости через providers: [...] — и они видны детям в шаблоне.

Когда ты просишь зависимость, Angular сначала ищет на узлах (снизу вверх по дереву шаблона), и только если там не нашёл — уходит в environment (ресепшн здания).

Главный миф: «инжектор на каждый элемент»

Очень частое заблуждение: «у каждого <div> свой инжектор». Это не так.

В рантайме узел (TNode) не получает инжектор автоматически. Слот под инжектор выделяется лениво — только если на узле реально есть что инжектить или что раздавать (директива с inject() в конструкторе, или компонент с providers).

Под капотом это даже не отдельный объект. Инжектор узла — это просто число (injectorIndex), указывающее на область из 9 ячеек внутри массива LView:

LView[injectorIndex .. injectorIndex+8]:
  [0..7] — bloom filter (256 бит, см. ниже)
  [8]    — ссылка на родительский инжектор + сам TNode

То есть «инжектор узла» = 9 чисел в общем массиве LView, а не класс с полями. Дёшево и быстро. Большинство <div> вообще никогда не получат этих 9 ячеек.

Bloom filter — «табличка на двери»

Представь: ты ищешь дрель и обходишь комнаты снизу вверх. Заходить в каждую и перебирать все ящики — долго. Поэтому на двери каждой комнаты висит табличка: «инструменты тут есть? да / нет». Не висит твоя пометка — проходишь мимо мгновенно.

Это и есть bloom filter — те самые 8 ячеек (256 бит). Каждому токену (классу сервиса) при первой регистрации присваивается номер, и в табличке зажигается соответствующий бит.

Поиск зависимости на узле:
1. bloomHasToken(token)?  бит НЕ горит → провайдера тут точно нет, идём выше
2. бит горит → МОЖЕТ быть (бывают ложные срабатывания) → честно проверяем
   директивы и providers этого узла

Главное свойство: «нет» — это всегда честное «нет» (мимо, не тратим время). «Да» — это «возможно, проверь внимательно». Поэтому поиск по дереву очень быстрый: 99% узлов отсеиваются одним сравнением бита.

Как идёт поиск: снизу вверх

Ты написал в компоненте svc = inject(DataService). Что делает Angular:

1. старт на текущем узле (твой компонент)
   → bloom: бит DataService горит? нет → выше
2. родительский узел в шаблоне
   → bloom: нет → выше
3. ... поднимаемся по TNode.parent до корня этого view
4. перепрыгиваем в родительский LView (если view вложенный)
5. дошли до верха дерева узлов — ничего → уходим в EnvironmentInjector
6. environment: DataService с providedIn:'root'? есть → создаём (один раз) и отдаём
7. нигде нет → ошибка NG0201: No provider for DataService

То есть путь всегда «комната → этаж → выше по этажам → ресепшн здания».

Флаги поиска — как менять маршрут

Иногда стандартный маршрут не подходит. Есть модификаторы (через inject(T, {...})):

{ self: true }      — искать ТОЛЬКО на своём узле, выше не подниматься
                      (нет тут — ошибка). «Спрашиваю только у себя в комнате»
{ skipSelf: true }  — пропустить свой узел, начать с родителя
                      «у себя не смотрю, спрашиваю соседей и выше»
{ host: true }      — подниматься, но не выше своего хост-компонента
                      «не выходить за пределы своей квартиры»
{ optional: true }  — не нашёл? верни null вместо ошибки
                      «нет фена — ну и ладно, обойдусь»

self и host ещё и запрещают уходить в environment injector — поиск заканчивается в дереве узлов.

providers vs viewProviders (коротко)

Компонент может раздавать сервис двумя способами:

@Component({
  providers:     [Svc],   // видят и дети в шаблоне, и спроецированный контент (ng-content)
  viewProviders: [Svc],   // видят ТОЛЬКО дети собственного шаблона, контент — НЕТ
})

viewProviders строже: проекция (<ng-content>) до них не дотянется. Внутри это один флаг canSeeViewProviders в фабрике инжектора.

Когда ты реально этим пользуешься

  • inject(Service) в компоненте/директиве — 99% случаев, маршрут «узлы → root».
  • providers: [...] на компоненте — дать детям свой экземпляр сервиса (например, отдельный стейт на каждую карточку списка).
  • @SkipSelf / skipSelf — классика для рекурсивных компонентов (дерево), чтобы взять родительский экземпляр, а не свой.
  • optional — для необязательных зависимостей (токен конфигурации, которого может не быть).

Итог одной картинкой

inject(Svc)
   │
   ▼
[узел] bloom: нет ──▶ [родитель] нет ──▶ ... ──▶ [корень view]
   │ да (проверь)                                     │ не нашли
   ▼                                                  ▼
директивы/providers узла                      EnvironmentInjector (root)
   │ нашли → отдать                                   │ нашли → создать(1 раз)+отдать
   ▼                                                  ▼
  готово                                       нет → NG0201 (или null если optional)

Запомни три вещи:

  1. инжектор на узле — это ленивые 9 чисел в LView, а не объект на каждый тег;
  2. bloom filter отсекает лишние узлы одним битом — поэтому поиск быстрый;
  3. маршрут всегда «снизу вверх по узлам → потом environment», а флаги (self/skipSelf/host/optional) этот маршрут подправляют.

Дальше → 10-vstroennye-direktivy: какие готовые директивы даёт Angular и как они связаны с порталами.