依賴注入是 Angular 的一大特性,通過它你能夠寫出更加易於維護的程式碼。但 JavaScript 語言本身並沒有提供依賴注入的功能,那麼 Angular 是如何實現依賴注入功能的呢?閱讀本文,你就能夠找到答案了。
一個典型的 Angular 應用程式,從開發者編寫的原始碼到在瀏覽器中執行,主要有 2 個關鍵步驟:
- 模板編譯,即通過執行
ng build
等構建命令呼叫編譯器編譯我們編寫的程式碼。 - 執行時執行,模板編譯的產物藉助執行時程式碼在瀏覽器中執行。
首先我們來編寫一個簡單的 Angular 應用,AppComponent 元件有個依賴項 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;
}
}
上面的程式碼經過 Angular 編譯器編譯打包後的產物大概是這樣:
由圖可知編譯的產物主要分為 HeroService 和 AppComponent 兩個部分,因為本文主要是講解依賴注入的實現原理,所以對於編譯產物的其他部分不做展開講解。現在我們來重點關注一下依賴注入相關的程式碼,其中箭頭所示程式碼:
AppComponent.ɵfac = function AppComponent_Factory(t) {
return new (t || AppComponent)(i0.ɵɵdirectiveInject(HeroService));
};
AppComponent_Factory 函式負責建立 AppComponent,顯而易見依賴項 HeroService 是通過 i0.ɵɵdirectiveInject(HeroService) 建立的。 我們繼續來看 i0.ɵɵdirectiveInject 函式做了什麼。
function ɵɵdirectiveInject(token, flags = InjectFlags.Default) {
// 省略無關程式碼
......
return getOrCreateInjectable(tNode, lView, resolveForwardRef(token), flags);
}
這裡我們直接定位到 getOrCreateInjectable 這個函式即可。在繼續分析這個函式之前,我們來先看看 lView 這個引數。在 Angular 內部,LView
和 [TView.data](http://TView.data)
是兩個很重要的檢視資料,Ivy(即 Angular 編譯和渲染管道)依據這些內部資料進行模板渲染。LView 被設計成一個單個的陣列,通過這個陣列包含了模板渲染需要的所有資料。TView.data
的資料可以被所有的模板例項共享。
現在我們回到 getOrCreateInjectable 這個函式:
function getOrCreateInjectable(tNode, lView, token, xxxxx) {
// 省略無關程式碼
......
return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue);
}
返回的是函式 lookupTokenUsingModuleInjector 執行的結果,通過名字大致可以瞭解到是通過模組注入器來查詢對應的 Token:
function lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue) {
// 省略無關程式碼
......
return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
}
moduleInjector.get 方法最終是由 R3Injector 去執行查詢:
this._r3Injector.get(token, notFoundValue, injectFlags);
}
這裡我們又引入了一個新的名詞:R3Injector。R3Injector 和 NodeInjector 是 Angular 中兩種不同型別的注入器。前者是模組層級的注入器,後者則是元件層級的。我們繼續看 R3Injector 的 get 方法做了什麼吧:
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);
}
通過上述程式碼,我們大概可以瞭解到 R3Injector 的 get 方法的大致流程。this.records 是一個 Map 集合,key 是 token, value 則是 token對應的例項。如果在 Map 集合中沒有找到對應的例項,就建立一條記錄。get 方法返回的 this.hydrate 函式執行的結果,這個函式最終執行的是本文開頭模板編譯產物中 HeroService.ɵfac 函式:
HeroService.ɵfac = function HeroService_Factory(t) {
return new (t || HeroService)();
};
至此 Angular 依賴注入的流程就分析完了。本文分析的程式碼示例使用的是模組注入器,那麼元件級別的注入器背後的實現流程是怎樣的呢?要使用元件級別的注入器,我們需要在@Component
裝飾器中顯式宣告 provider:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [{
provide: HeroService,
useClass: HeroService
}]
})
和模組注入器相同的流程就不再贅述,在 getOrCreateInjectable 函式中元件注入器關鍵函式如下:
function getOrCreateInjectable(tNode, lView, token, xxx) {
// 省略無關程式碼
......
const instance = searchTokensOnInjector(injectorIndex, lView, token, xxxx);
if (instance !== NOT_FOUND) {
return instance;
}
}
instance 是由函式 searchTokensOnInjector 建立的:
function searchTokensOnInjector(injectorIndex, lView, token, xxxx) {
// 省略無關程式碼
......
return getNodeInjectable(lView, currentTView, injectableIdx, tNode);
}
最終 getNodeInjectable 函式解釋了最終結果:
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
}
也就是說最開始我們分析的 i0.ɵɵdirectiveInject(HeroService) 函式建立的值,就是上面程式碼中的value。value 是由 factory.factory() 函式建立的,而 factory 函式依然是本文開頭模板編譯產物中 HeroService.ɵfac 函式。可以看到 R3Injector 和 NodeInjector 的區別在於,一個是通過 this.records 儲存依賴注入的例項,而 NodeInjector 則是通過 LView 儲存這些資訊。
本文首發於個人公眾號【朱玉潔的部落格】,後續將會持續分享前端相關技術文章,歡迎關注。