Angular dependency injection principle

Posted by djloc286 on Tue, 25 Jan 2022 21:41:01 +0100

Dependency injection is a major feature of Angular, through which you can write more maintainable code. But the JavaScript language itself does not provide the function of dependency injection, so how does Angular implement the function of dependency injection? Read this article and you will find the answer.

A typical Angular application, from the source code written by the developer to running in the browser, mainly has two key steps:

  1. Template compilation is to call the compiler to compile the code we write by running ng build and other construction commands.
  2. Run time, the product of template compilation runs in the browser with the help of run-time code.

First, let's write a simple Angular application. The AppComponent component has a dependency, HeroService:

import { Component } from '@angular/core';
import { Injectable } from '@angular/core';
@Injectable({
  providedIn: 'root'
})
export class HeroService {
  name = 'hero service';
  constructor() { }
}
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  title: string;
  constructor(heroService: HeroService) {
    this.title = heroService.name;
  }
}

After the above code is compiled and packaged by the Angular compiler, the product is roughly as follows:

It can be seen from the figure that the compiled product is mainly divided into HeroService and AppComponent. Because this paper mainly explains the implementation principle of dependency injection, other parts of the compiled product will not be explained. Now let's focus on the code related to dependency injection, where the arrow shows the code:

AppComponent.ɵfac = function AppComponent_Factory(t) {
  return new (t || AppComponent)(i0.ɵɵdirectiveInject(HeroService));
};

AppComponent_ The factory function is responsible for creating appcomponent. Obviously, the dependency HeroService is through i0 ɵɵ Created by directiveInject(HeroService). Let's continue to look at i0 ɵɵ What does the directiveInject function do.

function ɵɵdirectiveInject(token, flags = InjectFlags.Default) {
    // Omit irrelevant code
    ......
    return getOrCreateInjectable(tNode, lView, resolveForwardRef(token), flags);
}

Here, we can directly locate the function getOrCreateInjectable. Before continuing to analyze this function, let's take a look at the parameter LView. Inside Angular, LView and [TVIEW. Data]( http://TView.data )These are two very important view data. Ivy (i.e. Angular compilation and rendering pipeline) performs template rendering based on these internal data. LView is designed as a single array, which contains all the data required for template rendering. TView. Data can be shared by all template instances.
Now let's go back to the getOrCreateInjectable function:

function getOrCreateInjectable(tNode, lView, token, xxxxx) {
    // Omit irrelevant code
    ......
    return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue);
}

The returned result is the result of the function lookupTokenUsingModuleInjector. It can be seen from the name that the module injector is used to find the corresponding Token:

function lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue) {
   // Omit irrelevant code
   ......
   return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
}

moduleInjector. The get method is finally searched by R3Injector:

  this._r3Injector.get(token, notFoundValue, injectFlags);
}

Here we introduce a new term: R3Injector. R3Injector and NodeInjector are two different types of injectors in Angular. The former is a module level injector, while the latter is a component level injector. Let's continue to see what the get method of R3Injector does:

  get(token, notFoundValue = THROW_IF_NOT_FOUND, flags = InjectFlags.Default) {
    // Omit irrelevant code
    ......
    let record = this.records.get(token);
    if (record === undefined) {
      const def = couldBeInjectableType(token) && getInjectableDef(token);
      if (def && this.injectableDefInScope(def)) {
        record = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
      }
      else {
        record = null;
      }
        this.records.set(token, record);
    }
    // If a record was found, get the instance for it and return it.
    if (record != null /* NOT null || undefined */) {
      return this.hydrate(token, record);
}

Through the above code, we can roughly understand the general process of get method of R3Injector. this.records is a Map collection, key is token, and value is the instance corresponding to token. If no corresponding instance is found in the Map collection, a record is created. This returned by get method The result of the execution of the hydrate function. The final execution of this function is HeroService in the template compilation product at the beginning of this article ɵ fac function:

  HeroService.ɵfac = function HeroService_Factory(t) {
    return new (t || HeroService)();
  };

So far, the process of Angular dependency injection has been analyzed. The code example analyzed in this paper uses module injector, so what is the implementation process behind the Component level injector? To use the Component level injector, we need to explicitly declare the provider in the @ Component decorator:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [{
    provide: HeroService,
    useClass: HeroService
  }]
})

The same process as the module injector will not be repeated. In the getOrCreateInjectable function, the key functions of the component injector are as follows:

function getOrCreateInjectable(tNode, lView, token, xxx) {
    // Omit irrelevant code
    ......
    const instance = searchTokensOnInjector(injectorIndex, lView, token, xxxx);
  if (instance !== NOT_FOUND) {
    return instance;
  }
}

instance is created by the function searchTokensOnInjector:

function searchTokensOnInjector(injectorIndex, lView, token, xxxx) {
  // Omit irrelevant code
  ......
    return getNodeInjectable(lView, currentTView, injectableIdx, tNode);
}

The final getNodeInjectable function explains the final result:

export function getNodeInjectable(
    lView: LView, tView: TView, index: number, tNode: TDirectiveHostNode): any {
  let value = lView[index];
  const tData = tView.data;
    // ........
  if (isFactory(value)) {
    const factory: NodeInjectorFactory = value;
    try {
      value = lView[index] = factory.factory(undefined, tData, lView, tNode);
          // ...........
  return value
}

That is to say, at the beginning, we analyzed i0 ɵɵ The value created by the directiveInject(HeroService) function is the value in the above code. Value is defined by factory The factory () function is created, and the factory function is still HeroService in the template compilation product at the beginning of this article ɵ fac function. You can see the difference between R3Injector and NodeInjector is that one is through this Records stores instances of dependency injection, while NodeInjector stores this information through LView.

This article starts with the personal official account [Zhu Yujie's blog]. The follow-up will continue to share the front-end related technical articles, and welcome the attention.

Topics: Javascript angular TypeScript