What you always wanted to know about Angular Dependency Injection tree

If you didn’t know much about the Angular dependency injection system, you might now think that the root injector in an Angular application contains all merged service providers, that each component has its own injector, and that lazy-loaded modules have their own injector.

But maybe that’s not enough to know?

The Tree-Shakeable Tokens feature has been merged into the Master branch. If you are as curious as I am, you may want to know what changes this feature makes.

So go check it out now. Maybe something unexpected.

Injector Tree

Most developers know that Angular creates a root injector, where services are singletons. However, it looks like there are other injectors that are its parents.

As a developer, I want to know how Angular builds the injector tree. Here is the top part of the injector tree:

This is not the whole tree, we don’t have any components yet, we’ll continue to draw the rest of the tree. But now let’s take a look at the root Injector AppModule Injector because it’s the most commonly used.

Root AppModule Injector Root AppModule Injector

The Angular root Injector is the AppModule Injector shown above. The root Injector contains the service provider for all intermediate modules.

If we have a module with some providers and import this module directly in AppModule or in any other module, which has already been imported in AppModule, then those providers become application-wide providers.

By this rule, the MyService2 of EagerModule2 shown above will also be included in the root AppModule Injector.

Angular also adds ComponentFactoryResolver to the root injector object, which is used to create dynamic components because it stores the array of components pointed to by the entryComponents property.

Note that among all service providers are Module Tokens, which are the class names of the modules to be imported. We will return to the Module “Tree-shakeable Tokens” when we explore tree-Shakeable tokens.

Angular instantiates the root AppModule Injector AppModule Injector using the AppModule factory function inside the so-called module.ngFactory.js file:

We can see that the factory function returns a module object containing all the merged service providers, which should be familiar to all developers (see Angular’s @host decorator and element injector).

Tip: If you have angular application in dev mode and want to see all providers from root AppModule injector then just open devtools console and write:

ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._providers
Copy the code

There are a lot of other things that I’m not going to describe here because they are on the website:

Presents the IO/guide/ngmod…

Presents the IO/guide/hiera…

Platform Injector

In fact, the root AppModule Injector has a parent Injector, NgZoneInjector, which in turn is a child Injector of PlatformInjector.

PlatformInjector includes the built-in service provider when the PlatformRef object is initialized, but can also include additional service providers: PlatformInjector

const platform = platformBrowserDynamic([ { 
  provide: SharedService, 
  deps:[] 
}]);
platform.bootstrapModule(AppModule);
platform.bootstrapModule(AppModule2);
Copy the code

These additional service providers are passed in by us developers and must be StaticProviders. If you’re not familiar with the difference between StaticProviders and providers, check out this StackOverflow answer.

Tip: If you have angular application in dev mode and want to see all providers from Platform injector then just open devtools console and write:

ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._parent.parent._records;

// to see stringified value use
ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._parent.parent.toString();
Copy the code

While the process of dependency resolution by the root injector and its parent is clear, how dependencies are resolved by component-level injectors has puzzled me, so I went on to delve deeper.

EntryComponent and RootData

When I talked about ComponentFactoryResolver above, I referred to the entryComponents entry component. These entryComponents are declared in the NgModule’s bootstrap or entryComponents properties, and @angular/router uses them to dynamically create components.

Angular creates host factory functions for all entry components. These host factory functions are the root views of other views:

Every time we create dynamic component angular creates root view with root data, that contains references to 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

Suppose you’re running an Angular application.

What happens inside the following code when it executes:

platformBrowserDynamic().bootstrapModule(AppModule);
Copy the code

There’s actually a lot going on inside, but we’re only interested in how Angular creates entry components:

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

This is where the Angular injection tree bifurcates into two parallel trees.

Element Injector vs Module Injector

Not long ago, when lazy-loaded modules were widely used, someone on Github reported a strange case of dependency injection systems instantiating lazy-loaded modules twice. As a result, a new design was introduced. So, from there, Angular has two parallel injection trees: element injection tree and module injection tree.

The main rule is this: When a component or directive needs to resolve a dependency, Angular uses the Merge Injector to traverse the Element Injector Tree. If the dependency was not found, Angular iterates through the Module Injector tree to look for the dependency.

Please note I don’t use phrase “component injector” but rather “element injector”.

What is Merge Injector?

You’ve probably written something like this before:

@Directive({
  selector: '[someDir]'
}
export class SomeDirective {
 constructor(private injector: Injector) {}}Copy the code

This injector is the Merge Injector. Of course you can inject this Merge Injector into a component as well.

The Merge Injector object is defined as follows:

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

The code above shows that the Merge Injector is just a combination of views and elements. This Injector acts as a bridge between Element Injector Tree and Module Injector Tree when dependency resolution is used.

Merge Injector can also resolve built-in objects such as ElementRef, ViewContainerRef, TemplateRef, ChangeDetectorRef, and more interestingly, it can also return Merge Injector.

Basically every DOM element has a Merge Injector, even if no token is provided.

Tip: to get merge injector just open console and write:

ng.probe($0).injector
Copy the code

But you might ask what Element Injector is?

We know that @Angular/Compiler compiles the component template to generate a factory function that actually calls viewDef() to return a ViewDefinition object. The view is simply a representation of the template containing various types of nodes. Examples include directive, text, provider, and Query. The element node element node is used to represent DOM elements. In fact, the element injector, Element Injector, resides within this node. Angular stores all service providers on the element node in two properties of the node:

export interface ElementDef {
  ...
  /** * visible public providers for DI in the view, * as see from this element. This does not include private providers. */
  publicProviders: {[tokenKey: string]: NodeDef}|null;
  /** * same as visiblePublicProviders, but also includes private providers * that are located on this element. */
  allProviders: {[tokenKey: string]: NodeDef}|null;
}
Copy the code

Let’s look at how the element injector resolves dependencies:

constproviderDef = (allowPrivateServices ? elDef.element! .allProviders : elDef.element! .publicProviders)! [tokenKey];if (providerDef) {
  let providerData = asProviderData(searchView, providerDef.nodeIndex);
  if(! providerData) { providerData = { instance: _createProviderInstance(searchView, providerDef) }; searchView.nodes[providerDef.nodeIndex] = providerDataas any;
  }
  return providerData.instance;
}
Copy the code

Only the allProviders property is checked here, or the publicProviders property is checked for private purposes.

This injector contains the component/directive object and all of its service providers.

These service providers are mainly provided during the view instantiation phase by the ProviderElementContext object, which is also part of the @Angular/Compiler Angular compiler. If we dig deeper into this object, we find some interesting things.

For example, there are some limitations when using the @host decorator. You can use the viewProviders attribute of the Host element to get around these limitations. .

Another interesting thing is that if a directive is mounted on a component host element, but the component and directive provide the same token, then the service provider of the directive wins.

Tip: to get element injector just open console and write:

ng.probe($0).injector.elDef.element
Copy the code

Dependent parsing algorithm

The code for the view-dependency resolution algorithm is the resolveDep() function, which has been used by the Merge Injector get() method to resolve dependencies (services.resolvedep). To understand dependency resolution algorithms, we first need to know the concepts of views and superview elements.

If the root component had a template, we would have three views:

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

The dependency resolution algorithm resolves based on multi-level views:

If the child components need to parse, it presents will first find the component elements of the injector, also is check elRef. Element. AllProviders | publicProviders, then traverse upward check parent view elements of injector service providers (1), Until the parent element is equal to null (2), return the startView (3), then check that the startView. RootData. Elnjector (4), in the end, only token didn’t find, Again, check startView.rootData Module.Injector (5). (Note: Element injector -> Component injector -> module injector)

When a component view is traversed up to resolve a dependency, the parent element of the view is searched instead of the parent element of the element. Angular uses the viewParentEl() function to get the view parent:

/** * for component views, this is the host element. * for embedded views, this is the index of the parent node * that contains 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

For example, suppose we have a small program that looks like this:

@Component({
  selector: 'my-app',
  template: `<my-list></my-list>`
})
export class AppComponent {}

@Component({
  selector: 'my-list',
  template: `
    <div class="container">
      <grid-list>
        <grid-tile>1</grid-tile>
        <grid-tile>2</grid-tile>
        <grid-tile>3</grid-tile>
      </grid-list>
    </div>
  `
})
export class MyListComponent {}

@Component({
  selector: 'grid-list',
  template: `<ng-content></ng-content>`
})
export class GridListComponent {}

@Component({
  selector: 'grid-tile',
  template: `...`
})
export class GridTileComponent {
  constructor(private gridList: GridListComponent) {}
}
Copy the code

Assuming the Grid-Tile component depends on the GridListComponent, we can get the component object successfully. But how does it work?

What exactly is the superview element here?

The following steps answer this question:

  1. Find the starting element. The GridListComponent template contains grid-Tile element selectors, so you need to find elements that match the grid-tile selector. So the starting element is the grid-tile element.
  2. Find the template that has grid-tile elements, the MyListComponent component template.
  3. Determines the view for the element. Component view if there is no parent embedded view, embedded view otherwise. There is no ng-template or *structuralDirective on the grid-tile element, so here is the component view View_MyListComponent.
  4. Find the parent element of the view. This is the parent of the view, not the parent of the element.

There are two cases:

  • In the case of an embedded view, the parent element is the view container that contains the embedded view.

For example, suppose a grid-list is mounted with a structure directive:

@Component({
  selector: 'my-list',
  template: ` 
      
1 2 3
`
}) export class MyListComponent {} Copy the code

The parent element of grid-tile view is div.container.

  • For component views, the parent element is the host element.

Our little program above is the component view, so the superview elements are my-list elements, not grid-list.

Now, you might be wondering how Angular resolves GridListComponent dependencies if it skips grid-List.

The key is that Angular uses prototype chain inheritance to collect service providers:

Each time we provide a service provider for an element, Angular creates a new allProviders and publicProviders array that inherits from the parent. Otherwise, it only shares the two arrays of the parent.

This represents all the service providers that grid-tile contains all the parent elements in the current view.

The following diagram basically shows how Angular collects service providers for elements in templates:

As shown in the figure above, Grid-Tile successfully fetched the GridListComponent dependency via allProviders using the element injector because grid-Tile contains the service provider from the parent element.

To learn more, check out StackOverflow Answer.

The service provider of the element injector used stereotype chain inheritance, which prevented us from using the multi option to provide multiple services for the same token. But because of the dependency injection system is very flexible, there are ways to solve this problem, can check stackoverflow.com/questions/4…

You can keep the above explanation in mind, now continue to draw into the tree.

Simple my-app->child->grand-child application

Suppose we have the following simple program:

@Component({
  selector: 'my-app',
  template: `<child></child>`,})export class AppComponent {}

@Component({
  selector: 'child',
  template: `<grand-child></grand-child>`
})
export class ChildComponent {}

@Component({
  selector: 'grand-child',
  template: `grand-child`
})
export class GrandChildComponent {
  constructor(private service: Service) {}}@NgModule({
  imports: [BrowserModule],
  declarations: [
    AppComponent, 
    ChildComponent, 
    GrandChildComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Copy the code

We have a three-tier tree structure, and GrandChildComponent relies on Service:

my-app
   child
      grand-child(ask for Service dependency)
Copy the code

The following diagram illustrates how Angular internally resolves Service dependencies:

The figure above starts with the grand-child element of View_Child (1) and traverses up to find the parent element of all views. When the view has no parent element, in this example may-app has no parent element, use the injector of the root view to find (2) :

startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
Copy the code

In this example, startViet.root. injector is a NullInjector. Since NullInjector doesn’t have any service provider, Angular would have switched to the module injector:

startView.root.ngModule.injector.get(depDef.token, notFoundValue);
Copy the code

So Angular resolves dependencies in the following order:

AppModule Injector 
        ||
        \/
    ZoneInjector 
        ||
        \/
  Platform Injector 
        ||
        \/
    NullInjector 
        ||
        \/
       Error
Copy the code

The routing process

Let’s modify the program and add the router:

@Component({
  selector: 'my-app',
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent {}
...
@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      { path: 'child', component: ChildComponent },
      { path: ' ', redirectTo: '/child', pathMatch: 'full' }
    ])
  ],
  declarations: [
    AppComponent,
    ChildComponent,
    GrandChildComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
Copy the code

So the view tree looks something like:

my-app
   router-outlet
   child
      grand-child(dynamic creation)
Copy the code

Now let’s look at how routing creates dynamic components:

const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);                           
this.activated = this.location.createComponent(factory, this.location.length, injector);
Copy the code

Here Angular creates a new root view using the new rootData object and passes the OutletInjector as the root element injector elInjector. OutletInjector in turn relies on the parent injector this.Location. injector, which is the element injector for a router-outlet.

OutletInjector is a special injector that behaves a bit like a bridge between a router component and its parent, a router-outlet. The code for this object can be seen here:

Lazy loader

Finally, let’s move GrandChildComponent to the lazy loading module by adding a router-outlet to the child component and modifying the route configuration:

@Component({
  selector: 'child',
  template: ` Child 
       `
})
export class ChildComponent {}
...
@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: 'child', component: ChildComponent,
        children: [
          { 
             path: 'grand-child', 
             loadChildren: './grand-child/grand-child.module#GrandChildModule'}
        ]
      },
      { path: ' ', redirectTo: '/child', pathMatch: 'full' }
    ])
  ],
  declarations: [
    AppComponent,
    ChildComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
Copy the code
my-app
   router-outlet
   child (dynamic creation)
       router-outlet
         +grand-child(lazy loading)
Copy the code

Let’s draw two separate trees for lazy loaders:

Tree-shakeable tokens are on horizon

The Angular team has done a lot of subsequent work to make the framework smaller, starting with Version 6, providing another way to register service providers.

Injectable

A class previously decorated with Injectable does not indicate whether it has dependencies or not, nor how it is used. So if a service has no dependencies, the Injectable decorator can be removed.

As the API becomes stable, you can configure the Injectable decorator to tell Angular which module the service belongs to and how it was instantiated:

export interface InjectableDecorator {
  (): any; (options? : {providedIn: Type<any>| 'root' | null}&InjectableProvider): any;
  new (): Injectable;
  new(options? : {providedIn: Type<any>| 'root' | null}&InjectableProvider): Injectable;
}

export type InjectableProvider = ValueSansProvider | ExistingSansProvider |
StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider;
Copy the code

Here is a simple practical example:

@Injectable({
  providedIn: 'root'
})
export class SomeService {}

@Injectable({
  providedIn: 'root',
  useClass: MyService,
  deps: []
})
export class AnotherService {}
Copy the code

Unlike the ngModule Factory, which contains all service providers, information about service providers is stored in the Injectable decorator. This technique will make our application code smaller, because services that are not being used will be optimized by tree shaking. If we use Injectable to register service providers and the consumer does not import our service providers, then the packaged code does not include those service providers.

Prefer registering providers in Injectables over NgModule.providers over Component.providers

I mentioned Modules Tokens for root injectors at the beginning of this article, so Angular can distinguish which Modules appear in a particular module injector.

The dependency parser uses this information to determine whether the shakable tree optimization token belongs to the module injector.

InjectionToken

The InjectionToken object can be used to define how the dependency injection system constructs a token and to which injector the token applies:

export class InjectionToken<T> { constructor(protected _desc: string, options? : { providedIn? : Type<any>| 'root' | null, factory: () => T }) {} }Copy the code

So it should be used like this:

export const apiUrl = new InjectionToken('tree-shakeable apiUrl token', {                                   
  providedIn: 'root',                               
  factory: (a)= > 'someUrl'
});
Copy the code

conclusion

Dependency injection is a very complex topic in the Angular framework, and knowing its inner workings will give you more confidence in what you are doing, so I strongly recommend occasionally delve into Angular source code.

Note: This article is very deep, very long and very difficult, come on!