在Medium看到一篇Angular的文章,深入對比了 Constructor 和 ngOnInit 的不同,受益匪淺,于是搬過來讓更多的前端小伙伴看到,翻譯不得當之處還請斧正。
本文出處:The essential difference between Constructor and ngOnInit in Angular
難以譯出原意的術(shù)語都在圓括號里給出原詞了。下面開始正文!
在stackoverflow上被問得很多的一個關(guān)于Angular的問題就是Difference between Constructor and ngOnInit,閱讀量超過10萬。我回答了這個問題,但還是決定在這篇文章展開講一下。這上面的很多答案和網(wǎng)上的一些文章都把關(guān)注點放在這兩者用法的區(qū)別上,在這里我想給出一個深入到組件初始化進程的更全面的答案。
有關(guān)JS和TS語言的區(qū)別
讓我們從語言本身最明顯的區(qū)別開始。在一個類中,ngOnInit只是一個在結(jié)構(gòu)上與其他方法不一樣的方法。Angular團隊只是這樣命名它,但它也可以有其他任何名字:
class MyComponent {
ngOnInit() { }
otherNameForNgOnInit() { }
}
在一個組件類中引不引入這個方法完全取決于你。編譯過程中Angular編譯器會檢查組件是否引入了這個方法,然后用合適的標記去標記這個類:
export const enum NodeFlags {
...
OnInit = 1 << 16,
在變更檢測過程中,在組件實例內(nèi),這個標記會被用來決定是否調(diào)用ngOnInit方法:
if (def.flags & NodeFlags.OnInit && ...) {
componentClassInstance.ngOnInit();
}
相反,constructor是個不同的東西。在一個TypeScript類實例化過程中,無論寫不寫constructor,它都會被調(diào)用。這就是為什么一個TypeScript類的constructor會被轉(zhuǎn)譯成一個 JavaScript constructor function:
class MyComponent {
constructor() {
console.log('Hello');
}
}
轉(zhuǎn)譯成
function MyComponent() {
console.log('Hello');
}
在創(chuàng)建類實例時這個函數(shù)會被用new操作符調(diào)用:
const componentInstance = new MyComponent(
所以,如果你在類中省略constructor,這個類會被轉(zhuǎn)譯成一個空函數(shù):
function MyComponent() {}
這就是為什么我說在類中無論寫不寫constructor,它都會被調(diào)用。
有關(guān)組件初始化進程的區(qū)別
從組件初始化階段的角度看,兩者存在巨大差別。Angular bootstrap process(譯注:這個比較微妙,不知道怎么翻譯,暫且譯作引導進程吧)包含兩個主要階段:
- 構(gòu)造組件樹
- 運行變更檢測
而且,組件的constructor會在Angular構(gòu)造組件樹的時候被調(diào)用。所有生命周期鉤子包括ngOnInit會被作為接下來的變更檢測階段的一部分被調(diào)用。通常,組件初始化邏輯需要一些依賴注入提供商(DI providers),或者可用的輸入綁定,或者已渲染的DOM,在Angular 引導進程的不同階段,這些都是可用的。
Angular構(gòu)造組件樹的時候,根模塊注入器就已經(jīng)配置好,所以你可以注入任何全局依賴。而且,當Angular實例化一個子組件類的時候,父組件的注入器也已經(jīng)配置好,所以你可以注入父組件中定義的提供商(providers),包括父組件自身。組件的constructor是在注入器的上下文中被調(diào)用的唯一方法,所以如果你需要任何依賴,constructor是唯一獲得這些依賴的地方。@Input的通信機制(communication mechanism)是作為接下來的變更檢測階段的一部分處理的,所以輸入綁定在constructor中不可用。
Angular開始變更檢測的時候組件樹已經(jīng)構(gòu)造完畢,在組件樹中的所有組件的constructor都會被調(diào)用。而且這時候所有組件的模板節(jié)點(template nodes)也已經(jīng)添加到DOM中,這時,初始化組件的所有數(shù)據(jù)都已齊全——依賴注入提供商、DOM和輸入綁定(?DI providers, DOM and input bindings)。
你可以在Everything you need to know about change detection in Angular學習關(guān)于變更檢測的知識,在The mechanics of property bindings update in Angular學習Angular進程如何輸入。
我們用個簡單例子證明這些階段。假設有如下模板:
<my-app>
<child-comp [i]='prop'>
Angular開始引導應用程序。如上所述,它首先創(chuàng)建每個組件的類,因此調(diào)用MyAppComponent的constructor。當執(zhí)行組件的constructor時,Angular resolves(譯注:這個詞不知道怎么翻譯比較準確,就直接用原文了) 所有注入到MyAppComponentconstructor的依賴,并把他們作為參數(shù)提供出來(譯注:這里翻譯的比較拗口,原文是When executing a component constructor Angular resolves all dependencies that are injected into MyAppComponent constructor and provides them as parameters)。并且它會創(chuàng)建一個作為my-app宿主元素的DOM節(jié)點,然后它繼續(xù)創(chuàng)建child-comp的宿主元素,并且調(diào)用ChildComponent的constructor。在這個階段,Angular不關(guān)心i輸入綁定和任何生命周期鉤子。所以當這個過程完成的時候,Angular就創(chuàng)建出了如下組件視圖樹:
MyAppView
- MyApp component instance
- my-app host element data
ChildComponentView
- ChildComponent component instance
- child-comp host element data
直到那時Angular才會運行變更檢測、更新my-app的綁定、調(diào)用MyAppComponent實例的ngOnInit。然后它繼續(xù)更新child-comp的綁定和調(diào)用ChildComponent類的ngOnInit。
你可以在Here is why you will not find components inside Angular了解更多知識。
用法上的區(qū)別
現(xiàn)在從用法的角度看看兩者的區(qū)別。
Constructor
在Angular中,一個類的constructor主要用來注入依賴。Angular調(diào)用constructor injection pattern在這里已經(jīng)解釋得很詳細,更深入的見解你可以讀Mi?ko Hevery的文章Constructor Injection vs. Setter Injection。
然而,constructor的使用不僅限于依賴注入(DI)。舉個例子,@angular/router模塊的router-outlet指令在路由生態(tài)系統(tǒng)內(nèi)用constructor來注冊自己和自己的位置(viewContainerRef)。我在 Here is how to get ViewContainerRef before @ViewChild query is evaluated把它描述了一遍。
慣例就是,在constructor中,邏輯應盡可能少。
NgOnInit
前文我們看到,當Angular調(diào)用ngOnInit的時候,它已經(jīng)通過constructor完成創(chuàng)建組件DOM、注入所有必要的依賴,也已經(jīng)完成輸入綁定。這時所有必需信息已經(jīng)齊全,這些信息使得ngOnInit成為執(zhí)行初始化邏輯的好地方。
習慣上用ngOnInit來執(zhí)行初始化邏輯,即使這些邏輯不依賴于依賴注入(DI)、DOM或者輸入綁定。