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