Dependency injection is an Angular feature that allows you to write more maintainable code. But the JavaScript language itself does not provide dependency injection. How does Angular implement dependency injection? Read on to find out.

A typical Angular application takes two key steps from developer source code to running in a browser:

  1. Template compilation, which calls the compiler to compile the code we write by running build commands such as ng build.

  2. Run at runtime, the template-compiled artifacts run in the browser with runtime code.

Let’s start by writing a simple Angular app. The AppComponent has a HeroService dependency:

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; }}Copy the code

The code compiled and packaged by the Angular compiler looks something like this:

The compiled product is divided into HeroService and AppComponent. Since this article focuses on dependency injection, we won’t explain the rest of the compiled product. Now let’s focus on dependency injection related code, as shown by the arrow:

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

The AppComponent_Factory function creates the AppComponent dependency HeroService via i0.ɵɵdirectiveInject(HeroService). {}} {i0.ɵɵdirectiveInject function () {}

Function ɵɵdirectiveInject(token, flags = injectFlags.default) {// omit unrelated code...... return getOrCreateInjectable(tNode, lView, resolveForwardRef(token), flags); }Copy the code

GetOrCreateInjectable () is a function that we can locate. Before we go any further, let’s take a look at the lView argument. Inside Angular, LView and [tView.data](tview.data) are two important view data that Ivy (the Angular compile and Render pipeline) uses to render templates. The LView is designed as a single array that contains all the data needed for template rendering. The data in 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); }Copy the code

Return is the result of the function lookupTokenUsingModuleInjector followed by name is through modules can be roughly understanding to the injector to find the corresponding Token:

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

The moduleInjector.get method is finally done by R3Injector to look up:

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

Here we’ve introduced a new term: R3Injector. R3Injector and NodeInjector are two different types of Angular injectors. The former is a module-level injector, while the latter is component-level. Let’s continue with what R3Injector’s GET method has done:

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); }Copy the code

With the above code, we have a rough idea of the flow of R3Injector’s GET method. This. records is a Map collection where keys are tokens and values are instances of tokens. If no corresponding instance is found in the Map collection, a record is created. The get method returns the enclosing hydrate the results of the function, the function in the ultimate product execution is the beginning of the article template compilation HeroService. ɵ fac function:

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

This completes the Angular dependency injection process. The code examples examined in this article use a module injector, so what is the implementation flow behind a component-level injector? To use component-level injectors, 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
  }]
})
Copy the code

The same process is not needed for module injectable. The key functions of the component injector in the getOrCreateInjectable function are:

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

Instance is created by the function searchTokensOnInjector:

Function searchTokensOnInjector(injectorIndex, lView, Token, XXXX) {// Omit irrelevant code...... return getNodeInjectable(lView, currentTView, injectableIdx, tNode); }Copy the code

The getNodeInjectable function explains the 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 }Copy the code

{}} {i0.ɵ UseDirectiveInject (HeroService) {}} {}} {i0.ɵ UsedirectiveInject (HeroService) {}} {heroService.typicalfac}} Value is created by the factory.factory() function, which is also the heroService.typicalfac function from the template build at the beginning of this article. As you can see, the difference between R3Injector and NodeInjector is that one has stored an instance of dependency injection through this.records whereas NodeInjector has stored this information through LView.

This article was first published in personal wechat public number [Zhu Yujie’s blog], and will continue to share front-end related technical articles, welcome to pay attention to.