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 the design of multi-level dependency injection in Angular.

The Injectot injector, Provider Provider, and injector mechanism in Angular were introduced in the previous article. How do components and modules share dependencies in Angular applications, and can the same service be instantiated multiple times?

The dependency injection process for components and modules cannot be separated from Angular’s multi-level dependency injection design.

Multi-level dependency injection

As we said earlier, injectors in Angular are inheritable and hierarchical.

In Angular, there are two injector hierarchies:

  • ModuleInjectorModule injector: use@NgModule()or@Injectable()Annotations are configured in this hierarchyModuleInjector
  • ElementInjectorElement injector: Created implicitly on each DOM element

Module injectors and element injectors are both tree-structured, but their hierarchical structures are not exactly the same.

Module injector

The hierarchical structure of module injectors is related to the module design in the application, as well as the hierarchical structure of PlatformModule injectors and application module injectors.

PlatformModule injector

In Angular terms, a platform is the context in which Angular applications run. The most common platform for Angular applications is a Web browser, but it can also be an operating system or a Web server on a mobile device.

When an Angular app starts, it creates a platform layer:

  • Platforms are Angular’s entry point on a web page, and there is only one platform per page
  • Services shared by each Angular application running on the page are bound within the platform

An Angular platform that creates and destroys module instances:

@Injectable(a)export class PlatformRef {
  // Pass in an injector as a platform injector
  constructor(private _injector: Injector) {}

  // Create an instance of @NgModule for the given platform for offline compilationbootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options? : BootstrapOptions):Promise<NgModuleRef<M>> {}

  // Create an instance of @NgModule for the given platform using the given runtime compiler
  bootstrapModule<M>(
      moduleType: Type<M>,
      compilerOptions: (CompilerOptions&BootstrapOptions)|
      Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {}

  // Register the listener to call when the platform is destroyed
  onDestroy(callback: () = > void) :void {}

  // Get the platform injector
  The platform injector is the parent of every Angular application on the page and provides a singleton provider
  get injector() :Injector {}

  // Destroy the current Angular platform and all Angular applications on the page, including all modules and listeners registered on the platform
  destroy(){}}Copy the code

In fact, the platform created ngZoneInjector in ngzone. run at startup (in the bootstrapModuleFactory method) to create all the instantiated services in the Angular region. Applicationrefs (Angular applications running on the page) will be created outside the Angular region.

When launched in a browser, the browser platform is created:

export const platformBrowser: (extraProviders? : StaticProvider[]) = > PlatformRef =
    createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS);

// Where the platformCore platform must be included in any other platform
export const platformCore = createPlatformFactory(null.'core', _CORE_PLATFORM_PROVIDERS);
Copy the code

Creating a platform using a platform factory (such as createPlatformFactory above) implicitly initializes the platform of the page:

export function createPlatformFactory(parentPlatformFactory: ((extraProviders? : StaticProvider[]) => PlatformRef)|null, name: string,
    providers: StaticProvider[] = []) : (extraProviders? : StaticProvider[]) = >PlatformRef {
  const desc = `Platform: ${name}`;
  const marker = new InjectionToken(desc); / / DI token
  return (extraProviders: StaticProvider[] = []) = > {
    let platform = getPlatform();
    // If the platform is already created, no action is taken
    if(! platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS,false)) {
      if (parentPlatformFactory) {
        // If there is a parent platform, use the parent platform directly and update the corresponding provider
        parentPlatformFactory(
            providers.concat(extraProviders).concat({provide: marker, useValue: true}));
      } else {
        const injectedProviders: StaticProvider[] =
            providers.concat(extraProviders).concat({provide: marker, useValue: true}, {
              provide: INJECTOR_SCOPE,
              useValue: 'platform'
            });
        // If there is no parent platform, create a new injector and create a platform
        createPlatform(Injector.create({providers: injectedProviders, name: desc})); }}return assertPlatform(marker);
  };
}
Copy the code

The Angular application created the platform ModuleInjector, ModuleInjector, when creating the platform. As we saw from the Injector definition in the previous section, NullInjector is on top of all injectors:

export abstract class Injector {
  static NULL: Injector = new NullInjector();
}
Copy the code

So, on top of the platform module injector, there’s NullInjector(). Underneath the platform module injector, there is an application module injector.

Application root Module (AppModule) injector

Every application has at least one Angular module, and the root module is the module that launches the application:

@NgModule({ providers: APPLICATION_MODULE_PROVIDERS })
export class ApplicationModule {
  // ApplicationRef requires the bootstrap to provide the component
  constructor(appRef: ApplicationRef){}}Copy the code

The AppModule root application module is re-exported by BrowserModule and is automatically included in the root AppModule when we create a new application using the NEW command on the CLI. In the application root module, the provider is associated with a built-in DI token that is used to configure the root injector for the bootstrap.

Angular also adds ComponentFactoryResolver to the root module injector. This parser stores the entryComponents family of factories, so it is responsible for dynamically creating components.

Module injector level

At this point, we can simply tease out the hierarchy of module injectors:

  1. At the top of the module injector tree is the application root Module injector, called root.
  2. There are also two injectors on top of root, a PlatformModule injector and a PlatformModule injectorNullInjector().

Therefore, the module injector is layered as follows:

In our practical application, it is likely to look like this:

Angular DI has a hierarchical injection architecture, which means that lower-level injectors can create their own service instances as well.

Element injector

As mentioned earlier, Angular has two injector hierarchies: a module injector and an element injector.

The introduction of an element injector

When lazy-loaded modules became widely used in Angular, there was an issue where the dependency injection system caused lazy-loaded modules to double their instantiation.

In this fix, a new design was introduced: the injector uses two parallel trees, one for elements and one for modules.

Angular creates host factories for all entryComponents, which are the root views for all other components.

This means that every time we create a dynamic Angular component, we create a RootView using RootData:

class ComponentFactory_ extends ComponentFactory<any>{ create( injector: Injector, projectableNodes? :any[][], rootSelectorOrNode? :string|any, ngModule? : NgModuleRef<any>): ComponentRef<any> {
    if(! ngModule) {throw new Error('ngModule should be provided');
    }
    const viewDef = resolveDefinition(this.viewDefFactory);
    const componentNodeIndex = viewDef.nodes[0].element! .componentProvider! .nodeIndex;// Create a root view with root data
    const view = Services.createRootView(
        injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);
    // Accessor to view.Nodes
    const component = asProviderData(view, componentNodeIndex).instance;
    if (rootSelectorOrNode) {
      view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full);
    }
    // Create a component
    return new ComponentRef_(view, newViewRef_(view), component); }}Copy the code

The RootData contains references to the elInjector and ngModule injector:

function createRootData(
    elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2,
    projectableNodes: any[][], rootSelectorOrNode: any) :RootData {
  const sanitizer = ngModule.injector.get(Sanitizer);
  const errorHandler = ngModule.injector.get(ErrorHandler);
  const renderer = rendererFactory.createRenderer(null.null);
  return {
    ngModule,
    injector: elInjector,
    projectableNodes,
    selectorOrNode: rootSelectorOrNode,
    sanitizer,
    rendererFactory,
    renderer,
    errorHandler,
  };
}
Copy the code

Element injector trees were introduced because of the simplicity of the design. Change the injector hierarchy to avoid staggered insertion of modules and component injectors, resulting in double instantiation of lazy-loaded modules. Because each injector has only one parent object, and each parse must find exactly one injector to retrieve the dependencies.

Element Injector

In Angular, a view is a template representation that contains different types of nodes, including the element node on which the element injector resides:

export interface ElementDef {
  ...
  // The public provider of DI visible in this view
  publicProviders: {[tokenKey: string]: NodeDef}|null;
  // Same as visiblePublicProviders, but also includes private providers on this element
  allProviders: {[tokenKey: string]: NodeDef}|null;
}
Copy the code

By default ElementInjector is null unless configured in the providers property of @Directive() or @Component().

When Angular creates an element injector for a nested HTML element, it either inherits it from the parent or assigns the parent injector directly to the child node definition.

If an element injector on a child HTML element has a provider, it should inherit that injector. Otherwise, there is no need to create a separate injector for the child component, and dependencies can be resolved directly from the parent injector if desired.

Design of element injector and module injector

So where do element injectors and module injectors become parallel trees?

We already know that the Application Root Module (AppModule) is automatically included in the root AppModule when a new application is created using the NEW command on the CLI.

When the application (ApplicationRef) starts (bootstrap), an entryComponent is created:

const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
Copy the code

This procedure creates a RootView using RootData and creates a root Injector, where elInjector is injector.null.

Here, the Angular injector tree is split into an element injector tree and a module injector tree, which are parallel trees.

Angular regularly creates child injectors. Every time Angular creates a Component instance that specifies providers in @Component(), it creates a new child injector for that instance. Similarly, when a new NgModule is loaded at runtime, Angular can create an injector for it with its own provider.

Submodules and component injectors are independent of each other and create their own instances of the services they provide. When Angular destroys NgModule or component instances, it also destroys those injectors and the service instances in those injectors.

Angular resolves dependencies

We introduced two injector trees in Angular: module injector tree and element injector tree. How does Angular resolve dependencies when providing them?

In Angular, when resolving a token acquisition dependency for a component/directive, Angular resolves it in two phases:

  • forElementInjectorHierarchy (parent)
  • forModuleInjectorHierarchy (parent)

The process is as follows (see multi-level injector-parsing rules) :

  1. When a component declares a dependency, Angular tries to use its ownElementInjectorTo satisfy the dependency.
  2. If a component’s injector lacks a provider, it passes the request to its parent’sElementInjector.
  3. These requests continue to be forwarded until Angular finds an injector that can handle the request or runs out of ancestorsElementInjector.
  4. If Angular is in anyElementInjectorWill be returned to the element that initiated the request, and will be found inModuleInjectorHierarchy to find.
  5. Angular raises an error if it still can’t find the provider.

Angular introduces a special merge injector to do this.

Merge Injector

The merge injector itself does not have any values; it is just a combination of view and element definitions.

class Injector_ implements Injector {
  constructor(private view: ViewData, private elDef: NodeDef|null) {}
  get(token: any.notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
    const allowPrivateServices =
        this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) ! = =0 : false;
    return Services.resolveDep(
        this.view, this.elDef, allowPrivateServices,
        {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); }}Copy the code

When Angular parses dependencies, the merge injector is a bridge between the element injector tree and the module injector tree. When Angular tries to resolve some dependency in a component or directive, it uses the merge injector to traverse the element injector tree, and then switches to the module injector tree to resolve the dependency if it cannot find it.

class ViewContainerRef_ implements ViewContainerData {...// The parent attempts to query the element injector
  get parentInjector() :Injector {
    let view = this._view;
    let elDef = this._elDef.parent;
    while(! elDef && view) { elDef = viewParentEl(view); view = view.parent! ; }return view ? new Injector_(view, elDef) : new Injector_(this._view, null); }}Copy the code

The parsing process

The injector is inheritable, which means that if the specified injector cannot resolve a dependency, it asks the parent injector to resolve it. The specific parsing algorithm is implemented in resolveDep() :

export function resolveDep(
    view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef,
    notFoundValue: any = Injector.THROW_IF_NOT_FOUND) :any {
  //
  // mod1
  // /
  // el1 mod2
  / / / /
  // el2
  //
  // When requesting el2.injector.get(token), check in the following order and return the first value found:
  // - el2.injector.get(token, default)
  // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module
  // - mod2.injector.get(token, default)
}
Copy the code

If it is the root AppComponent of a template such as

, then Angular has three views:

<! -- HostView_AppComponent -->
    <my-app></my-app>
<! -- View_AppComponent -->
    <child></child>
<! -- View_ChildComponent -->
    some content
Copy the code

Depending on the parsing process, the parsing algorithm is based on the view hierarchy, as shown in the figure below:

If you parse some tokens in a child component, Angular will:

  1. First check by looking at the child element injectorelRef.element.allProviders|publicProviders.
  2. It then iterates through all the superview elements (1) and checks the providers in the element injector.
  3. If the next superview element is equal tonull(2), then return tostartView(3), checkstartView.rootData.elnjector(4).
  4. Check only if the token cannot be foundstartView.rootData module.injector(5).

As you can see, Angular searches for the parent of a particular view, not the parent of a particular element, when iterating through a component to resolve some dependencies. The parent element of a view can be obtained by:

// For component views, this is the host element
// For embedded views, this is the index containing the parent node of the view container
export function viewParentEl(view: ViewData) :NodeDef|null {
  const parentView = view.parent;
  if (parentView) {
    returnview.parentNodeDef ! .parent; }else {
    return null; }}Copy the code

conclusion

This article focuses on the injector hierarchy in Angular. There are two parallel injector trees in Angular: module injector tree and element injector tree.

The element injector tree is introduced to solve the problem of double instantiation of modules when dependency injection resolution is lazily loaded. Angular’s dependency resolution process was tweaked after the introduction of the element injector tree to prioritize looking for dependencies in injectors such as element injectors and superview element injectors, and only looking up dependencies in module injectors if the token cannot be found in the element injector.

reference

  • Angular- Multilevel injectors
  • What you always wanted to know about Angular Dependency Injection tree
  • A curious case of the @Host decorator and Element Injectors in Angular