Dependency injection is a great feature of Angular that allows you to 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 2 key steps:
- Template compilation, that is, calling the compiler to compile the code we wrote
ng build
- Run at runtime, the product of template compilation is run in the browser with the help of runtime 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;
}
}
The product of the above code compiled and packaged by the Angular compiler is probably like this:
It can be seen from the figure that the compiled product is mainly divided into two parts: HeroService and AppComponent. Because this article mainly explains the implementation principle of dependency injection, the 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));
};
The AppComponent_Factory function is responsible for creating the AppComponent, obviously the dependency HeroService is created through i0.ɵɵdirectiveInject(HeroService). Let's move on to what the i0.ɵɵdirectiveInject function does.
function ɵɵdirectiveInject(token, flags = InjectFlags.Default) {
// 省略无关代码
......
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 lView parameter. Inside Angular, LView
and [TView.data](http://TView.data)
are two important view data, and Ivy (ie, the Angular compilation and rendering pipeline) performs template rendering based on these internal data. LView is designed as a single array that contains all the data needed for template rendering. TView.data
data can be shared by all template instances.
Now we go back to the getOrCreateInjectable function:
function getOrCreateInjectable(tNode, lView, token, xxxxx) {
// 省略无关代码
......
return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue);
}
What is returned is the result of the execution of the function lookupTokenUsingModuleInjector. From the name, it can be roughly understood that the module injector is used to find the corresponding Token:
function lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue) {
// 省略无关代码
......
return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
}
The moduleInjector.get method is ultimately used by R3Injector to perform the lookup:
this._r3Injector.get(token, notFoundValue, injectFlags);
}
Here we have introduced a new term: R3Injector. R3Injector and NodeInjector are two different types of injectors in Angular. The former is a module-level injector, and 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) {
// 省略无关代码
......
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 probably understand the general process of the get method of R3Injector. this.records is a Map collection, the key is the token, and the value is the instance corresponding to the token. If no corresponding instance is found in the Map collection, a record is created. The result of the execution of the this.hydrate function returned by the get method, this function finally executes the HeroService.ɵfac function in the template compilation product at the beginning of this article:
HeroService.ɵfac = function HeroService_Factory(t) {
return new (t || HeroService)();
};
At this point, the process of Angular dependency injection is analyzed. The code example analyzed in this article uses the 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 @Component
@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. The key functions of the component injector in the getOrCreateInjectable function are as follows:
function getOrCreateInjectable(tNode, lView, token, xxx) {
// 省略无关代码
......
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) {
// 省略无关代码
......
return getNodeInjectable(lView, currentTView, injectableIdx, tNode);
}
Finally the getNodeInjectable function explains the end 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, the value created by the i0.ɵɵdirectiveInject(HeroService) function that we analyzed at the beginning is the value in the above code. The value is created by the factory.factory() function, and the factory function is still the HeroService.ɵfac function in the template compilation product at the beginning of this article. It can be seen that the difference between R3Injector and NodeInjector is that one stores the instance of dependency injection through this.records, while NodeInjector stores the information through LView.
This article was first published on the personal public account [Zhu Yujie's blog], and will continue to share front-end related technical articles in the follow-up. Welcome to pay attention.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。