Angular

Иерархия инжекторов

Один и тот же сервис может быть единым на всё приложение, а может существовать в стольких экземплярах, сколько на экране открыто карточек. Решает это не сам сервис, а место, где зарегистрирован его провайдер. Angular держит не один инжектор, а целое дерево: корневой на приложение, и по инжектору на каждый компонент, который объявил свои провайдеры. Когда компонент просит зависимость, поиск идёт вверх по этому дереву до первого совпадения. Понимание этого дерева - граница между 'сервис случайно стал общим' и осознанным выбором области видимости.

  • Angular CDK Overlay: каждый модальный диалог получает собственный инжектор, так что данные диалога не утекают наружу
  • Формы: NgControl и провайдеры валидаторов разрешаются от ближайшего родителя-формы вверх по дереву
  • Списки карточек: компонент-карточка с провайдером в своих providers даёт каждой карточке отдельный экземпляр стейт-сервиса
  • Angular Material: компоненты ищут токены конфигурации сначала локально, затем в корне, что позволяет точечно переопределять оформление
  • Тестирование: TestBed подменяет провайдеры на нужном уровне, изолируя компонент от реального дерева приложения

Предварительные знания

  • Функция inject() и провайдинг сервиса через providedIn: 'root'
  • Рецепты провайдеров: useClass, useValue, useFactory, useExisting
  • Понимание дерева компонентов: компоненты вкладываются друг в друга

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

Angular не хранит зависимости в одном месте. Есть два уровня иерархии. Первый - окружение: корневой EnvironmentInjector, созданный при старте приложения, плюс инжекторы окружения у ленивых маршрутов. Второй - элементы: у каждого компонента, объявившего поле providers, появляется собственный инжектор элемента, привязанный к его позиции в DOM-дереве. Эти инжекторы выстраиваются в иерархию, повторяющую вложенность компонентов.

  • providedIn: 'root' — Регистрация в корневом инжекторе. Один общий экземпляр на всё приложение, переживает смену маршрутов. Tree-shakable: исчезает из бандла, если ни один код его не инжектит.
  • providers в @Component — Регистрация в инжекторе элемента. Новый экземпляр на каждый инстанс компонента, живёт ровно столько, сколько компонент в DOM. Виден только самому компоненту и его потомкам.

Время жизни сервиса привязано к его инжектору. Корневой живёт всё приложение. Сервис из providers компонента уничтожается вместе с компонентом, и его ngOnDestroy вызывается - удобно для очистки подписок, привязанных к жизни компонента.

Компонент-карточка объявляет CardStateService в своём поле providers. На экране отрисовано восемь карточек. Сколько экземпляров CardStateService создаст Angular?

Разрешение вверх по дереву

Когда компонент просит зависимость, Angular начинает с инжектора элемента самого компонента. Если регистрации там нет, поиск поднимается к инжектору родителя, затем выше, и так до корневого EnvironmentInjector. Побеждает первое найденное совпадение. Если до корня ничего не нашлось, по умолчанию это ошибка NullInjectorError.

Это объясняет переопределение конфигов. Если корень провайдит APP_CONFIG, а конкретное поддерево объявляет свой APP_CONFIG в providers, компоненты внутри поддерева получат локальную версию: поиск находит её раньше корневой. Снаружи поддерева продолжает действовать корневая. Так настраивается точечное переопределение без глобальных эффектов.

Резолюция всегда движется только вверх, к предкам, и никогда вбок к сиблингам или вниз к потомкам. Поэтому сервис, объявленный в providers соседнего компонента, текущему компоненту недоступен: их инжекторы не лежат на одном пути к корню.

Компонент A объявляет сервис в своих providers. Компонент B - сосед A (общий родитель), не предок и не потомок. Что произойдёт, когда B попробует заинжектить этот сервис?

Опции inject: optional, self, skipSelf, host

По умолчанию inject() ищет зависимость от текущего инжектора до корня и кидает ошибку, если не нашёл. Второй аргумент с опциями меняет и стартовую точку, и поведение при отсутствии. Это даёт точный контроль над тем, откуда брать зависимость.

ОпцияЧто меняетКогда применяют
optional: trueВозвращает null вместо ошибки, если провайдер не найденНеобязательная зависимость, у которой есть запасной путь
self: trueИщет только в текущем инжекторе, не поднимается вышеЗависимость обязана быть объявлена локально на этом компоненте
skipSelf: trueПропускает текущий инжектор, начинает с родителяДоступ к родительской версии в обход локального переопределения
host: trueОстанавливает поиск на хост-компонентеДиректива берёт зависимость только в пределах своего хоста

self и skipSelf - взаимоисключающие по смыслу: self запирает поиск в текущем инжекторе, skipSelf запрещает в нём искать. Указывать обе сразу нет смысла. host применяется в основном директивами, чтобы не утягивать зависимости из произвольных предков за пределами своего хост-компонента.

Компонент локально переопределяет TreeContext в своих providers, но в одном месте ему нужна именно родительская версия контекста. Какая опция inject это даёт?

Связь с другими темами

Урок про области видимости DI. Эти же механизмы лежат под маршрутизацией и формами:

  • InjectionToken и провайдеры — Рецепты, которые здесь размещаются на разных уровнях дерева
  • Параметры, resolvers и guards — У маршрутов есть свой уровень инжектора, провайдеры маршрута живут между корнем и компонентом

Итог

  • Инжекторы образуют дерево: корневой EnvironmentInjector на приложение и инжектор элемента на каждый компонент со своими providers
  • providedIn: 'root' кладёт сервис в корень - один экземпляр на приложение, tree-shakable если не используется
  • providers в @Component создаёт новый экземпляр на каждый инстанс этого компонента - область видимости сужается до поддерева
  • Разрешение идёт вверх по дереву от текущего инжектора к корню, побеждает первое совпадение
  • Опции inject управляют поиском: optional разрешает null, self ограничивает текущим инжектором, skipSelf начинает с родителя, host останавливается на хост-компоненте

Связанные уроки

  • ng-19-injection-tokens — Рецепты провайдеров - предпосылка: иерархия отвечает на вопрос, на каком уровне эти провайдеры разместить
  • ng-17-di-intro — Базовое DI и providedIn: 'root' - отправная точка для разговора об уровнях инжекторов
Иерархия инжекторов

0

1

Войти