閱讀 Angular 6/RxJS 最新教程,請訪問 前端修仙之路
本文基於 Top Common Mistakes of Angular Developers 這篇文章的內容進行整理和擴展,建議有興趣的讀者直接閱讀原文。若是你剛接觸 Angular,也能夠參考一下 Angular 常見問題彙總 這篇文章。javascript
Angular 1.x 版本統稱爲 AngularJS,Angular 2+ (4/5) 統稱爲 Angular。前端
第三方庫的命名也有必定的規則。假設早期版本的命名以 ng-
做爲前綴,當 Angular 2 發佈後,該庫名稱會使用 ng2-
做爲前綴。但當 Angular 4 發佈之後,新的命名規則就隨之出現了。新的術語是使用 ngx-
做爲前綴,由於 Angular 使用語義版本,每六個月會發佈一個新版本。所以,舉個例子,當咱們把 ng2-bootstrap
改名爲 ngx-bootstrap
後,從此就不須要再頻繁更換庫的名稱了。java
AngularJS 使用 watcher
和 listener
的概念。watcher 是一個函數,返回被監測的值。一般狀況下,這些值是對象模型的屬性值。但也不老是數據模型的屬性 - 咱們能夠跟蹤組件的狀態、計算新值等。若是該函數返回的值與前一次的值不同,Angular 就會調用 listener
,一般它用來更新 UI 狀態。jquery
Angular 移除了 watch
和 scope
,如今咱們將使用組件的輸入屬性。除此以外,Angular 爲咱們提供了 ngOnChanges
生命週期鉤子。爲了提升變化檢測的性能,對於對象比較,Angular 內部直接使用 ===
運算符進行值比較。所以當輸入屬性是引用類型,當改變對象內部屬性時,是不會調用 ngOnChanges
生命週期鉤子的。git
// JS has NaN !== NaN export function looseIdentical(a: any, b: any): boolean { return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b); }
許多開發人員不知道這一點,陷入這個陷阱。爲了解決這個問題,有各類解決方案:github
ngDoCheck
生命週期鉤子subscriptions
使用 ngDoCheck
生命週期掛鉤是解決此問題的經常使用方法。當變化檢測運行時會自動調用此鉤子。在使用今生命週期鉤子時,你要當心控制該鉤子的內部邏輯,由於一般每分鐘會觸發屢次變化檢測 (能夠參考下面的源碼)。typescript
ngStyle
指令內部也實現了DoCheck
接口,而後利用KeyValueDiffer
對象來檢測對象的變化 (如內部屬性的新增、修改、移除操做)。
// packages/core/src/view/provider.ts // 變化檢測: // checkAndUpdateView -> Services.updateDirectives(view, CheckType.CheckAndUpdate) function checkAndUpdateDirectiveInline( view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, v8: any, v9: any): boolean { const providerData = asProviderData(view, def.index); const directive = providerData.instance; let changed = false; let changes: SimpleChanges = undefined !; const bindLen = def.bindings.length; // 判斷輸入屬性值是否改變,若發生改變則更新changes對象相應的屬性。 if (bindLen > 0 && checkBinding(view, def, 0, v0)) { changed = true; changes = updateProp(view, providerData, def, 0, v0, changes); } // ... if (bindLen > 9 && checkBinding(view, def, 9, v9)) { changed = true; changes = updateProp(view, providerData, def, 9, v9, changes); } // 若輸入屬性發生變化纔會調用ngOnChanges生命週期鉤子 if (changes) { directive.ngOnChanges(changes); } // 若首次執行變化檢測及實現OnInit生命週期鉤子,則調用ngOnInit生命週期鉤子 if ((view.state & ViewState.FirstCheck) && (def.flags & NodeFlags.OnInit)) { directive.ngOnInit(); } // 若實現DoCheck接口,則調用ngDoCheck生命週期鉤子 if (def.flags & NodeFlags.DoCheck) { directive.ngDoCheck(); } return changed; // 返回SimpleChanges對象 }
你可能知道當你訂閱 Observable 對象或設置事件監聽時,在某個時間點,你須要執行取消訂閱操做,進而釋放操做系統的內存。不然,你的應用程序可能會出現內存泄露。json
@Component({ ... }) export class HeroComponent implements OnInit, OnDestroy { heroForm: FormGroup; valueChanges$: Observable; ngOnInit() { this.valueChanges$ = this.heroForm.valueChanges.subscribe(...); } ngOnDestroy() { this.valueChanges$.unsubscribe(); } }
大多數狀況下,當你在組件類中執行訂閱操做,你能夠在 ngOnDestroy
生命週期鉤子中,執行取消訂閱的操做。bootstrap
上面介紹了在某些場景下須要手動執行取消訂閱操做,進而釋放相應的資源。但有些場景下,無需咱們開發者手動執行額外的取消訂閱操做。由於在這些場景下,Angular 內部會自動執行取消訂閱操做。好比,使用 async
的場景:segmentfault
@Component({ selector: 'heroes-garden', template: `<hero [hero]="heroes$ | async"></todos>` }) export class HeroesGardenComponent implements OnInit, OnDestroy { heroesChanged$: Observable; ngOnInit() { this.heroesChanged$ = this.store.select('heroes'); } ngOnDestroy() { this.heroesChanged$.unsubscribe(); } }
除了使用 async
的場景外,還有如下場景會自動取消訂閱:
若想進一步瞭解手動釋放資源和自動釋放資源的場景,能夠參考專欄 Angular 中什麼時候取消訂閱 這篇文章。
分層依賴注入做爲 Angular 的新機制的一部分,讓咱們能夠靈活地控制依賴注入。在 AngularJS 中,服務都是單例的,而 Angular 2.x 以上的版本,咱們能夠屢次實例化一個服務。
假設咱們已經定義了一個 HeroesService
服務,用來獲取英雄信息:
@Injectable() export class HeroesService { heroes: Hero[] = []; constructor(private http: Http) { this.http.get('http://give-me-heroes.com') .map(res => res.json()) .subscribe((heroes: Hero[]) => { this.heroes = heroes; }); } getHeroes() { return this.heroes; } }
正如你所見,咱們在構造函數中獲取英雄的數據,此外咱們定義了 getHeroes()
方法,用來獲取英雄信息。
如今咱們來使用剛建立的 HeroesService
服務:
@Component({ selector: 'hero', template: '...', providers: [HeroesService] }) export class HeroComponent { constructor(private heroesService: HeroesService) {} } @NgModule({ declarations: [HeroComponent] } export class HeroesModule { ... }
在 HeroComponent
中,咱們在 @Component.providers
數組中聲明 HeroesService
服務,而後在 HeroComponent
組件類的構造函數中注入該服務。使用這種方式會有問題,每當實例化新的 HeroComponent
組件時,都會建立一個新的 HeroService
實例,這會致使發送屢次 Http 請求。
解決上述問題的一種方案是在 @NgModule.providers
中聲明服務。
@Component({ selector: 'hero', template: '...' }) export class HeroComponent { constructor(private heroesService: HeroesService) {} } @NgModule({ declarations: [HeroComponent], providers: [HeroesService] } export class HeroesModule { ... }
採用這種方式的話,對於多個 HeroComponent
組件,HeroesService
服務只會被實例化一次。由於,當在模塊中聲明 provider
,它所相關的依賴對象,將是單例的,其它的模塊都可以使用它。咱們不須要經過 @NgModule.exports
數組來導出對應的 provider
,它會被自動導出。
Angular 再也不是簡單的 Web 框架,Angular 是一個平臺,它的一個優勢是容許咱們將應用程序代碼與渲染器分離,從而編寫能夠在瀏覽器、服務器上運行的應用程序,甚至能夠編寫原生應用。
此外解耦後,也爲咱們提供更多的能力,如使用 AOT (Ahead of time) 或 Web Worker。AOT 意味着在構建階段進行模板編譯,AOT 編譯模式的開發流程:
運行 ngc 編譯應用程序
除此以外 AOT 還有如下優勢:
若是咱們如今或未來要使用這種功能,咱們須要遵照必定的規則。其中一個規則是不能使用 jQuery,document 對象或 ElementRef.nativeElement 來直接操做 DOM。具體示例以下:
@Component({ ... }) export class HeroComponent { constructor(private _elementRef: ElementRef) {} doBadThings() { $('.bad-with-jquery').click(); this._elementRef.nativeElement.xyz = 'bad with native element'; document.getElementById('bad-with-document'); } }
如你所見,doBadThings()
方法中有三行代碼,這三行代碼演示了直接操做 DOM 的三種方式。在 Angular 中咱們推薦經過 Renderer2
服務執行 DOM 操做 (Angular 2 中使用 Renderer)。
@Component({ ... }) export class HeroComponent { constructor( private _renderer2: Renderer2, private _elementRef: ElementRef) {} doGoodThings() { this._renderer2.setElementProperty(this._elementRef, 'some-property', true); } }
上面代碼中,咱們經過依賴注入方式注入 Renderer2
和 ElementRef
實例,而後在 doGoodThings()
方法中調用 Renderer2
實例提供的 setElementProperty()
方法來設置元素的屬性。 此外,爲了方便開發者獲取視圖中的元素,Angular 爲咱們提供了 @ViewChild
、@ViewChildren
、@ContentChild
和 @ContentChildren
等裝飾器。
渲染器是視圖層的封裝。當咱們在瀏覽器中時,將使用默認渲染器。當應用程序在不一樣平臺 (如 WebWorker ) 上運行時,渲染器將被替換爲平臺對應的渲染器。此渲染器須要實現 Renderer2
抽象類,並利用 DI (依賴注入) 機制做爲默認的 Renderer 對象注入到組件或服務中。
若想深刻了解 Angular 渲染器,能夠參考專欄 Angular Renderer (渲染器) 這篇文章。
組件是 Angular 應用程序中的常見構建塊。每一個組件都須要在 @NgModule.declarations
數組中聲明,纔可以使用。
在 Angular 中是不容許在多個模塊中聲明同一個組件,若是一個組件在多個模塊中聲明的話,那麼 Angular 編譯器將會拋出異常。例如:
@Component({ selector: 'hero', template: '...', }) export class HeroComponent { ... } @NgModule({ declarations: [HeroComponent] } export class HeroesModule { ... } @NgModule({ declarations: [HeroComponent] } export class AnotherModule { ... }
如你所見,HeroComponent
組件在 HeroesModule 以及 AnotherModule 中進行聲明。在多個模塊中使用同一個組件是容許的。但當這種狀況發生時,咱們應該考慮模塊之間的關係是什麼。若是一個模塊做爲另外一個模塊的子模塊,那麼針對上面的場景解決方案將是:
@NgModule.declaration
中聲明 HeroComponent
組件@NgModule.exports
數組中導出該組件@NgModule.imports
數組中導入子模塊而對於其它狀況,咱們能夠建立一個新的模塊,如 SharedModule
模塊。具體步驟以下:
NgModule({ declarations: [HeroComponent], exports: [HeroComponent] } export class SharedModule { ... } NgModule({ imports: [SharedModule] } export class HeroesModule { ... } @NgModule({ imports: [SharedModule] } export class AnotherModule { ... }