Angular позволяет создавать компоненты, каналы, директивы и многое другое. Для настройки необходимых зависимостей Angular предоставляет технологию под названием Dependency Injection (внедрение зависимостей).

Есть два механизма  —  провайдер зависимостей (dependency provider) и потребитель зависимостей (dependency consumer). Взаимодействие между ними возможно благодаря абстрактному объекту, называемому инжектором (injector).

Рассмотрим пример.

Добавление декоратора Injectable к классу показывает, что он доступен для внедрения и может быть внедрен.

@Injectable()
class OneService {}

Теперь OneService действует как зависимость, которая может быть внедрена в несколько мест.

Где она может быть внедрена?

  1. На уровне компонента.
  2. На уровне модуля приложения.

P. S. Рассмотрим уровень модуля отдельно (в условиях отложенной загрузки).

На уровне компонента можно просто добавить провайдеры в декоратор компонента:

@Component({
selector: 'app-list',
template: '...',
providers: [oneService]
})
class TestComponent {}

На уровне модуля аналогично предоставляем зависимость в провайдеры в декораторе NgModule:

@NgModule({
selector: 'app-list',
template: '...',
providers: [oneService]
})
class TestComponent {}

Из компонента также можно непосредственно добавить зависимость на корневом уровне, т.е. с помощью встроенного модуля, используя providedIn:'root'.

На данный момент это наиболее часто используемый вариант:

@Injectable({
providedIn: 'root'
})
class TestService {}

После добавления зависимости в провайдеры можно использовать ее и внедрять в элементы там, где это требуется. 

Самый распространенный способ внедрения зависимости  —  добавление ее в конструктор соответствующего компонента, где нужно использовать эту зависимость.

constructor(private loggerService:LoggerService) { }

Ранее я упомянул об абстрактном объекте  —  инжекторе, управляющим взаимодействием между потребителями и провайдерами.

Посмотрим, как это происходит.

Повторное использование экземпляра

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

P. S. Для проверки наличия экземпляров используется восходящий подход к разрешению, который будет продемонстрирован ниже.

Экземпляр относительно определенного уровня

Обсудим еще один момент, связанный с экземпляром. Рассмотрим пример, когда зависимость внедряется на уровне компонента.

Предположим, у нас два компонента, и оба они требуют одну и ту же зависимость, которую мы обеспечили на уровне каждого компонента.

Соответственно у этих двух компонентов будут созданы разные экземпляры.

А когда мы предоставляем зависимость на корневом уровне, т. е. на уровне модуля приложения, экземпляр, созданный для зависимости, остается неизменным во всем приложении.

Теперь следует ответить на вопрос: почему мы определяем экземпляр в модуле приложения, а не в компоненте приложения?

Дело в том, что в соответствии с иерархией модулей Angular модуль приложения находится выше, чем компонент приложения.

Примечание: над модулем приложения (App Module) находится модульный инжектор (Module injector), настраиваемый модулем платформы (Platform Module), а еще выше  —  нулевой инжектор (Null Injector), на котором все заканчивается. За более подробной информацией можно обратиться к официальной документации.

Область применения провайдера

Как ограничить область применения провайдера?

В модуле с динамической загрузкой инжектор в корне делает все упомянутые провайдеры доступными в начале работы приложения.

Теперь предположим, что на определенном маршруте мы отложено загружаем модуль, и он требует соответствующие зависимости, которые указаны в его массиве провайдеров, т.е. провайдеров для конкретного модуля. Но ведь корневой компонент не знает об этом.

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

Созданный отложено загруженный модуль имеет свой экземпляр.

Он может быть реализован следующим образом:

import { Injectable } from ‘@angular/core’;

@Injectable({
 providedIn: ‘any’,
})
export class SomeService {}

Помните, мы обсуждали внедрение сервисов на уровне компонента? Это называется ограничением области применения провайдеров на уровне компонента.

Теперь вы можете спросить: если я внедрил одну и ту же зависимость (сервис) на уровне модуля приложения и на уровне компонента, то как Angular узнает, какой экземпляр использовать?

Дело в том, что есть набор правил разрешения, которым следует Angular во избежание любого типа конфликта.

Разрешение происходит в два этапа.

1. ModuleInjector. Он может быть настроен одним из двух способов с помощью:

  • свойства @Injectable() providedIn;
  • массива провайдеров @NgModule().

2. ElementInjector. Предоставление сервиса в декораторе @Component() с помощью провайдеров или свойства viewProviders настраивает ElementInjector.

Когда компонент объявляет зависимость, Angular прежде всего пытается удовлетворить эту зависимость с помощью своего ElementInjector. Если это не удается, то запрос передается вверх к ElementInjector родительского компонента, пока Angular не найдет инжектор, способный обработать запрос, или не исчерпает иерархию родительских компонентов ElementInjector.

Для разрешения запроса используется восходящий подход.

Если Angular не удалось найти провайдера ни в одной иерархии ElementInjector, т.е. в провайдерах компонентного уровня, он возвращается к элементу, в котором возник запрос, и проводит поиск в иерархии ModuleInjector. Если и после этого провайдер не будет найден, Angular выдаст ошибку.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Vikas Tiwari: Dependency Injection-Behind the Scenes

Предыдущая статьяРеализация структурированной конкурентности в Java и Kotlin
Следующая статья7 способов сократить код JavaScript