This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

As a front-end framework designed for “big front-end projects,” Angular has a lot of design to learn from, and this series focuses on how those designs and features work. This article focuses on dependency injection, the biggest feature in Angular, and introduces some basic concepts in the Angular dependency injection architecture.

Dependency injection

To introduce dependency injection design for the Angular framework, let’s introduce the basic concepts of dependency injection. We often confuse the dependency inversion principle (DIP), inversion of control (IoC), and dependency injection (DI), so here’s a brief introduction.

Dependency inversion principle, inversion of control, dependency injection

Low coupling and high cohesion are probably one of the design goals of every system, and many design patterns and ideas have been generated for this purpose, including the design idea of relying on inversion principle and inversion of control.

(1) Dependency inversion principle (DIP).

The original definition of dependency inversion principle is:

  • A high-level module should not depend on a low-level module; both should depend on its abstraction;
  • Abstractions should not depend on details, details should depend on abstractions.

To put it simply: modules should not rely directly on each other, but should rely on an abstract rule (interface or abstract class).

(2) Inversion of control (IoC).

Inversion of control is defined as: dependencies between modules are instantiated externally from within the program. That is, when an object is created, it is controlled by an external entity that regulates all objects in a system and passes (injects) references to the objects on which it depends.

There are two main ways to achieve inversion of control:

  • Dependency injection: Passively receives dependent objects
  • Dependency lookup: Actively solicit dependent objects

(3) Dependency injection.

Dependency injection is the most common technique for inversion of control.

Dependency inversion and inversion of control are complementary and can often be used together to effectively reduce coupling between modules.

Dependency injection in Angular

In Angular, the DI framework also uses dependency injection. When a class is instantiated, the DI framework provides it with declared dependencies (dependencies: services or objects that the class needs to perform its functions).

Dependency injection in Angular revolves around components or modules and provides dependencies for newly created components.

The main dependency injection mechanism in Angular is the injector mechanism:

  • Any dependencies needed in an application must register a provider using the application’s injector so that the injector can use the provider to create new instances
  • Angular creates application-wide injectors and other injectors as needed during startup

The two main concepts involved here are the Injector and the Provider. Let’s take a look.

Injector Injector

The Injector is used to create dependencies and maintains a container to manage these dependencies and reuse them as much as possible. The injector provides a singleton of dependencies and injects this singleton into multiple components.

Obviously, as a container for creating, managing, and maintaining dependencies, the functions of an injector are simple: create, get, and manage dependency instances. We can also see this in the source code for the abstract Injector class:

export abstract class Injector {
  // Dependencies are not found
  static THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;
  // NullInjector is the top of the tree
  // If you go too far up the tree to look for a service in NullInjector, you'll get an error message or, in the case of @optional (), null
  static NULL: Injector = new NullInjector();

  // Retrieved an instance from the Injector based on the provided Token
  abstractget<T>( token: Type<T> | AbstractType<T> | InjectionToken<T>, notFoundValue? : T, flags? : InjectFlags ): T;// Create a new Injector instance that provides one or more dependencies
  static create(options: {
    providers: StaticProvider[]; parent? : Injector; name? :string;
  }): Injector;

  // Const const const const const const const const const const const const injectable ()
  // It defines how the DI system will construct tokens, and which Injector is available
  staticɵ prov = ɵ ɵ defineInjectable ({token: Injector,
    providedIn: "any" as any.} // Inject new ɵɵ Inject directive: Injected Token from the currently active Injector
    factory: () = >ɵ ɵ inject (INJECTOR)});static __NG_ELEMENT_ID__ = InjectorMarkers.Injector;
}
Copy the code

That is, we can add the dependency instances that need to be shared to the injector and retrieve the corresponding dependency instances through Token queries and retrieves the injector.

Note that injectors in Angular are hierarchical, so the process of finding dependencies is also a process of walking up the injector tree.

This is because in Angular, applications are organized as modules, as shown in 5. Modular Organization. Typically, the DOM of a page is a tree with HTML as the root node, and components and modules in Angular applications are built on that tree.

Injectors, on the other hand, serve components and modules, which are also mounted on a tree of modules and organizations. As a result, the Injector has also been divided into module and component levels, providing specific instances of dependencies for components and modules, respectively. Injectors are inheritable, which means that if the specified injector cannot resolve a dependency, it will ask the parent injector to resolve it, as we can also see from the code creating the injector above:

// Create a new Injector instance that can be passed to the parent Injector
static create(options: {providers: StaticProvider[], parent? : Injector, name? :string}): Injector;
Copy the code

Within the scope of an injector, services are singletons. That is, there can be at most one instance of a service in a specified injector. If you do not want to use the same instance of the service everywhere, you can share instances of a service dependency on demand by registering multiple injectors and associating them with components and modules as needed.

We can see that when creating a new Injector instance, the parameters passed in include Provider. This is because the Injector does not create dependencies directly, but through the Provider. Each injector maintains a list of providers and uses them to provide instances of services based on the needs of components or other services.

The Provider Provider

The Provider Provider is used to tell the injector how to obtain or create dependencies. For the injector to be able to create services (or provide other types of dependencies), the injector must be configured with a Provider.

A provider object defines how to obtain injectable dependencies associated with DI tokens, which the injector uses to create instances of the classes on which it depends.

About DI tokens:

  • When you configure an injector using a provider, you associate the provider with a DI token;
  • The injector maintains an internal token-provider mapping table that is referenced when a dependency is requested, and the token is the key of the mapping table.

There are many types of providers, and you can read their definitions in the official documentation:

export type Provider =
  | TypeProvider
  | ValueProvider
  | ClassProvider
  | ConstructorProvider
  | ExistingProvider
  | FactoryProvider
  | any[];
Copy the code

The provider resolution process is as follows:

function resolveReflectiveFactory(
  provider: NormalizedProvider
) :ResolvedReflectiveFactory {
  let factoryFn: Function;
  let resolvedDeps: ReflectiveDependency[];
  if (provider.useClass) {
    // Use classes to provide dependencies
    const useClass = resolveForwardRef(provider.useClass);
    factoryFn = reflector.factory(useClass);
    resolvedDeps = _dependenciesFor(useClass);
  } else if (provider.useExisting) {
    // Use existing dependencies
    factoryFn = (aliasInstance: any) = > aliasInstance;
    // Get specific dependencies from tokens
    resolvedDeps = [
      ReflectiveDependency.fromKey(ReflectiveKey.get(provider.useExisting)),
    ];
  } else if (provider.useFactory) {
    // Use the factory method to provide dependencies
    factoryFn = provider.useFactory;
    resolvedDeps = constructDependencies(provider.useFactory, provider.deps);
  } else {
    // Use provider-specific values as dependencies
    factoryFn = () = > provider.useValue;
    resolvedDeps = _EMPTY_LIST;
  }
  //
  return new ResolvedReflectiveFactory(factoryFn, resolvedDeps);
}
Copy the code

The internal resolution representation of the provider used by the Injector has been obtained after parsing based on different types of providers:

export interface ResolvedReflectiveProvider {
  // key, including system-wide unique IDS, and a token
  key: ReflectiveKey;
  // A factory function that returns an instance of an object represented by a key
  resolvedFactories: ResolvedReflectiveFactory[];
  // Indicate whether the provider is multi-provider or regular provider
  multiProvider: boolean;
}
Copy the code

The provider can be the service ClassProvider itself, and if a service class is specified as a provider token, the default behavior of the injector is to instantiate that class with new.

Dependency injection services in Angular

In Angular, a service is a class with the @Injectable decorator that encapsulates non-UI logic and code that can be reused in the application. Angular separates components and services to improve modularity and reusability.

Mark a class with @Injectable to ensure that the compiler will generate the necessary metadata (metadata is also an important part of Angular) when injecting the class to create class dependencies.

The @Injectable decorator class gets an Angular Injectable object when compiled:

// Compile Angular Injectable objects based on their Injectable metadata and patch the result
export function compileInjectable(type: Type<any>, srcMeta? : Injectable) :void {
  // The compiler relies on @angular/compiler
  // See the compiler's implementation of compileFactoryFunction compileInjectable
}
Copy the code

InjectableDef defines how the DI system constructs tokens and which injectors (if any) are available:

export interfaceɵ ɵ InjectableDef < T > {// Specify that a given type belongs to a specific injector, including root/platform/any/null and a specific NgModule
  providedIn: InjectorType<any> | "root" | "platform" | "any" | null;
  // The token to which this definition belongs
  token: unknown;
  // The factory method to execute to create injectable instances
  factory: (t? : Type<any>) = > T;
  // Store the location of injectable instances in the absence of an explicit injector
  value: T | undefined;
}
Copy the code

When using providedIn from @Injectable(), the optimization tool can do tree-shaking optimization to remove unused services from the application to reduce bundle size.

conclusion

This article briefly introduces several key concepts in Angular dependency injection, including Injector, Provider, and Injectable.

Injectors, providers, and injectable services can be understood simply as follows:

  1. Injectors are used to create dependencies and maintain a container to manage these dependencies and reuse them as much as possible.
  2. A dependent service in an injector, with only one instance.
  3. The injector needs to use a provider to manage dependencies and to correlate them through tokens (DI tokens).
  4. The provider uses how the high-speed injector should obtain or create dependencies.
  5. The injectable service class is compiled from the metadata to produce an injectable object that can be used to create instances.

reference

  • Angular- Dependency injection in Angular
  • Angular- Depends on the provider