前言
之前曾經介紹到 Angular 元件生命週期的 Hook分別是 ngOnInit 與 ngOnDestroy ,在那篇文章內曾經說過元件被實體化的過程,第一個先執行的是建構式 constructor ,也提到盡量不要再建構式裡面寫程式碼。這次要介紹的式另一個生命週期的 Hook - ngOnChanges 。
ngOnChanges() 、 constructor() 、 ngOnInit()
在介紹之前,來一張元件生命週期的圖表:
ngOnChanges() 與 constructor() 、 ngOnInit() 之間的執行順序是如何,可以很容易地從這張圖表上看出來。
但秉持著實踐精神,我們還是實際寫一些程式測試看看~
1 | import { Component, OnInit, Input, OnChanges } from '@angular/core'; |
因為有跑 ngFor 而文章資料總共有 6 筆,因此產生 6 個 ArticleBody 元件。
- 可以從這裡觀察出無論如何一定會先執行 constructor() ,但執行過程中元件還沒被初始化。
- 此時元件內什麼東西都沒有,也沒有透過屬性繫結傳進來的資料。
- 緊接著執行 ngOnChanges() 然後才是 ngOnInit()
這就是它們的執行順序。
什麼是 ngOnChanges()
ngOnChanges() 觸發的時機點
- 元件裡面的屬性如果有套用 @input() ,只要該元件的父元件透過屬性繫結傳資料進來的話, ngOnChanges() 就會被觸發
驗證 ngOnChanges() 觸發的時機點
- 在 ArticleBody 元件內新增一個屬性
counter
counter
值會從父元件透過屬性繫結傳入- 當
counter
發生改變時 ngOnChanges() 就會觸發,由此驗證
ArticleBody
1 | export class ArticleBodyComponent implements OnInit, OnChanges { |
1 | <article class="post" id="post{{idx}}" *ngFor="let item of atticleData; let idx = index"> |
ArticleList
1 | export class ArticleListComponent implements OnInit { |
為了方便所以補上 item.id
,接著運行開發伺服器,觀察 console.log
變化:
由此可知,在兩秒後六個不同的 ArticleBody 元件都觸發了 ngOnChanges() ,證明了只有在屬性繫結傳入資料發生改變時才會觸發 ngOnChanges() 。
ngOnChanges() 參數的作用
另外 ngOnChanges() 可以傳入一個參數,比較傳入前與傳入後的資料:
1 | ngOnChanges(changes) { |
發現這個 changes
接收到一個叫物件,點開後會發現裡面還有一層以 counter
屬性命名的物件:
- 該物件的型別為 SimpleChange
- 代表當前值為多少的屬性
currentValue
- 代表前一個值為多少的屬性
previousValue
firstChange
是不是第一次發生改變- 目前已經是第二次改變了,所以是
false
- 第一次改變發生於重新整理時那一次的 ngOnChanges()
- 目前已經是第二次改變了,所以是
透過這個例子得知可以利用這個參數在觸發 ngOnChanges() 時多做一些額外的判斷。
ngOnChanges() 實務應用
前面幾篇文章提到,建議不要在子元件上直接使用雙向繫結修改傳入的資料,因為這樣會直接的修改到原始資料。
但現在我們懂得使用 ngOnChanges() 這個 Hook ,因此可以針對這個部分對整個程式碼進行重構。
ActicleHeader
切回 ActicleHeader 元件觀察:
- 目前有一個
@input()
綁定了item
屬性- 所以當接到
item
資料時 ngOnChanges() 會被觸發
- 所以當接到
- 透過
ngOnChanges(changes)
,當接收到值時建立一份與原始資料不同的新物件- 如此一來就可以放心地使用雙向繫結,也不怕修改到原始資料
1 | import { Component, OnInit, Input, Output, EventEmitter, OnChanges} from '@angular/core'; |
接著修改 Template
- 在 Input 內補上雙向繫結
- 因為改為使用雙向繫結
editTitle()
與editExit()
不再需要傳入$event
參數
- 因為改為使用雙向繫結
- 調整取消編輯上的點擊事件,當點擊時觸發
editExit()
1 | <header class="post-header"> |
因為調整了 Template ,所以 class 內的方法也需要跟著調整。
邏輯部分
- 新增了一個屬性
originData
用途是保存原始傳入的資料,用於取消編輯時使用- 並於 ngOnChanges() 觸發時將原始資料賦值給
originData
- 特別注意這裡不可以寫
this.originData = this.item;
因為此時還沒有item
- 特別注意這裡不可以寫
- 並於 ngOnChanges() 觸發時將原始資料賦值給
- 刪除不再需要的屬性
newTitle
- 調整
editTitle()
,此時發射的this.item
已經不是原始資料了 - 調整
editExit()
,實作不可變物件特性,使用原始資料重新建立新物件還原
1 | import { Component, OnInit, Input, Output, EventEmitter, OnChanges} from '@angular/core'; |
現在 ArticleHeader 這個元件邏輯已經完成,可以測試一下。
- 倘若這個步驟沒辦法完成,可能是雙向繫結出了問題
- 應檢查是否有在 Article 功能模組內匯入
FormsModule
,需匯入才可在 input 上使用雙向繫結
- 應檢查是否有在 Article 功能模組內匯入
至此程式的運作都如預期,唯獨刪除功能出狀況了,所以我們到 ArticleList 父元件調整一下程式碼。
ArticleList
- 造成刪除失敗的原因是,現在的 item 物件已經不是原本的 item 物件了
- 而任意兩個物件在 JavaScript 中相比都是 False ,因此刪除導致失敗
- 修正方法為改採
item.id
比較即可
因此程式碼修改如下:
1 | doDelete(item){ |
測試看看是否如我們預期。
小結
透過 ngOnChanges() 的特性在屬性繫結的階段時,讓 item
屬性變成是一個「不可變的物件」,因為只是有任何新的資料進來,馬上就會重新產生新的物件,並且賦值給 item
,藉由這種方式讓 item
值與原本傳進子物件的 item
值得以脫鉤。
脫鉤後就可以在 Template 內放心地使用雙向繫結而不會影響到父物件內的原始資料,這麼做也確保了修改 item
屬性時不會一併的修改到其他元件內 item
屬性的內容。