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)
Запомни три вещи:
- инжектор на узле — это ленивые 9 чисел в
LView, а не объект на каждый тег; - bloom filter отсекает лишние узлы одним битом — поэтому поиск быстрый;
- маршрут всегда «снизу вверх по узлам → потом environment», а флаги
(
self/skipSelf/host/optional) этот маршрут подправляют.
Дальше → 10-vstroennye-direktivy: какие готовые директивы даёт Angular и как они связаны с порталами.