Angular 雙向繫結的二三事
Angular 雙向繫結
利用一個例子來看看破壞雙向繫結
// parent component html
<child [val]='val'/>
<button (onClick)="val=1">Reset</button>
// child component html
<span>{{val}}</span>
<button type="button" (onClick)="val=val+1">increase</button>
parent component 傳遞一個val給child component, 同時child component 內部有個button ,點選會加1,那麼現在進行如下操作,首先,val 的值是1, 然後,我們點選 child component 內部的button, 最後,我們點選parent component內部的button reset. 那麼val 值在兩個元件中的值為多少,顯示為多少。 大家猜猜,結果看下圖。
步驟 | val in parent | val in child | val in display |
---|---|---|---|
初始 | 1 | 1 | 1 |
點選btn in child | 1 | 2 | 2 |
點選btn in parent | 1 | 2 | 2 |
為什麼,Angular 怎麼不工作了
先思考這幾個問題
- Angular 是如何發現元件的差別,從而觸發更新的
Angular 在每個元件內部(具體放在哪裡的還得看原始碼,但是肯定是跟元件物件instance 關聯),快取了所有的Input, 每次在檢查元件是否要更新時,都會用最新的Input 更快取的Input 進行比較,找到差異,差異在ngOnChanges() hook 函式內體現。有兩個問題,- 比較什麼,
以上面的例子為例,假設我們正在對parent component 進行changeDection, 首先,我們比較的東西是<child [val]='val'/>
ngOnChanges(changes:SimpleChanges)
拿到需要更新的Inputs。 - 怎麼比較?
我猜跟React 裡面類似吧,說白了就是物件比較引用,primitive type 直接值比較
- 比較什麼,
那我們來分析一下上面例子。
步驟 | val cache of child in parent | val in child |
---|---|---|
初始化後 | 1 | 1 |
點選increase in child | 1 | 2 |
點選reset in parent | 1 | 2 |
在我們點選increase in child 後,在parent component 進行changedetection 時,val 在parent 內部,已經快取的值都是1, 所以,對於parent component 而言,child component 的Input 沒有變化,當我們點選reset 時同樣的,val在parent 內部還是1,跟快取的值一樣,所以,child component Input 還是沒有變化,Angular不會將1賦值給child component, 但是child component 內部 val 值一直是2, 這就是為什麼Angular 不工作的原因。
怎麼解決這個問題?
問題的根源是在parent 內部,child Input val 快取的值跟child 內部實際的val 值不一致。那麼我們有兩個思路,
- 第一種,我們只要告訴Angular 或者,讓Angular感知到val 變化了,看如下程式碼
<button (onClick)="val=3;setTimeout(()=>val=1,0)">Reset</button>
我們先把val設定成一個第三狀態值,然後,非同步再設定成我們想要的值,先設定成第三狀態值,是為了區別於快取的1,然後Angular 更新值快取,接著後面的setTimeout,把值再設定成我們要的值,Angular 再次介入,更新。這是一種hack的方式。
- 第二種方法,我們利用雙向繫結,也就是child 的內部不會直接更新val 的值,而是把更新的值通知parent ,由parent 來更新,然後在changedetection 裡面,child 拿到更新後的值,更新DOM, 如果有同學熟悉React 的話,這就是controlled component 跟uncontrolled component。Angular 裡面提供了語法糖,不用像React 那麼累。看程式碼
// parent component
<child [(val)]='val'/>
// child component ts
@Input()
val:any;
@Output()
valChange:EventEmitter<any>=new EventEmitter<any>()
increase(){
this.valChange.emit(this.val+1);
}
<button type="button" (onClick)="increase()">increase</button>
當點選child 裡面的increase button 時,child component 並不直接修改val,而是把修改後的值通知parent,parent利用語法糖,自動更新val ,在隨後的changedetection中,把更新後的值賦給child.
this.valueChange.emit()是不是實時的? 答案是:是的
如果Input 不是一個primitive type,而是一個物件,會怎麼樣,如果是物件一些就簡單多了,因為物件是地址引用的比較,所以只有引用不變,Angular 一律認為沒有改變。
這裡強調一點,儘管Angular 發現一些元件的Input 沒有發生變化,Angular 還是會遞迴的對其內部的元件做ChangeDetection,以及Dom的重新整理,ChangeDetection 的目的是發現差異,並且將差異傳遞給目標元件,如果目標是native html element, 那麼就是更新Dom,如果是component,direcrive, 那麼就更新Input,然後遞迴ChangeDetection.(不管目標元件有沒有Input 更新,都會做,當然有特殊,例如Onpush,等)