以推導的方式解釋 Two-Way Binding

Interpolation binding 與 property binding 都當 class 的 field 有變動時,會自動反應到 class 的 field,若 HTML 有任何變動也能反應到 class 的 field,這就是 two-way binding 了。

Version


Angular 4.3

One-Way Binding


如何使用 Angular 實作下拉選單? 一文中,我們使用了 HTML template reference varible 方式,也使用 DOM event 方式,無論哪種方法,使用的是 one-way binding 的技術。

twoway000

  1. Interpolation Binding:當 class 的 field 資料改變,HTML 會自動改變
  2. Event Binding:當 HTML 資料改變,發動 event,由 class 的 method 去修改 field

Two-Way Binding


twoway001

  1. Two Way Binding:資料不用透過 method 修改,當 HTML 改變時,field 會跟著改變;且當 field 改變時,HTML 也會隨之改變。

土炮 Two-Way Binding


Two-way binding 理念很棒,利用既有的 property binding、event binding、interpolation binding,該如何寫出 two-way binding 呢?

src/app/app.component.html

1
2
3
4
<select id="TDDSelect" [value]="selectedId" (change)="selectedId = $event.target.value">
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p>{{ selectedId }}</p>
1
[value]="selectedId"

使用 property binding 將 value 直接綁定到 selectedId field,也就是當 selectedId field 有任何修改,都會反應到 selectedIdvalue

1
(change)="selectedId = $event.target.value"

使用 event binding 將 change event 直接綁定到雙引號內的 statement,也就是將 $event.target.value 直接指定給 selectedId field。

1
<p>{{ selectedId }}</p>

使用 interpolation binding 當 selectedId field 有任何變動,HTML 會自動跟著變化。

透過 property binding 與 event binding 的組合,當 HTML 改變時,field 會跟著改變;且當 field 改變時,HTML 也會隨之改變。

src/app/app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
import { Cloud } from './cloud';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
selectedId = '0';

clouds: Cloud[] = [
{id: 0, name: 'AWS'},
{id: 1, name: 'Azure'},
{id: 2, name: 'Aliyun'}
];
}

Class 部份完全不用任何 method 幫忙,只剩下 field 部分。

這樣就完成了我們土炮的 two-way binding 了。

使用 ngModel


雖然土炮的方式可行,Angular 為了讓我們更方便,特別設計了 ngModel directive,讓我們使用更簡單的語法就能完成 two-way binding。

src/app/app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

12 行

1
2
3
4
imports: [
BrowserModule,
FormsModule
],

ngModel directive 需要使用 FormsModule,而 Angular CLI 預設沒有載入,必須手動加上。

src/app/app.component.html

1
2
3
4
<select id="TDDSelect" [ngModel]="selectedId" (ngModelChange)="selectedId = $event">
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p>{{ selectedId }}</p>
  1. [ngModel]="selectedId" :使用 property binding,將 selectedId field 綁定到 ngModel directive
  2. (ngModelChange)="selectedId = $event":使用 event binding,當 ngModelChange event 時,執行 selectedId = $event

value attribute 換 ngModel directive 較無感,但由 input event 換 ngModelChange event 就比較有感了,因為 $event.target.value 換成 $event 更為精簡。

這也意味著 Angular 內部會自己將處理 $event.target.value

若仔細去看,會發現 (ngModelChange)$event 是贅字,因此可以再次簡化。

src/app/app.component.html

1
2
3
4
<select id="TDDSelect" [(ngModel)]="selectedId">
<option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
</select>
<p>{{ selectedId }}</p>

(ngModelChange)$event 再次被簡化,將 ()[] 合一,就變成了 [(ngModel)]="selectedId" 了。

[()] 語法稱為 Banana in a Box,將 property binding 與 event binding 合而為一。

Recap


  1. 原本的 value attribute 抽象化成 ngModel directive。
  2. 原本的 change event 抽象化成 ngModelChange event,change event 會觸發 ngModelChange event
  3. 原本的 $event.target.value 簡化成 $event,Angular 會在內部處理
  4. ()[] 合一成為 [()]

ngModel 內部運行機制


twoway002

  1. 在 HTML 修改觸發 DOM 的 change event
  2. change event 觸發了 Angular 的 ngModelChange event
  3. $event 傳給 selectedId field
  4. ControlValueAccessor$event.target.value 指定給 selectedId field
  5. selectedId field 被修改,引發 interpolation binding 自動更新 HTML

步驟 4 就是將 $event.target.value 轉成 field 的黑魔法所在

是否該使用 ngModel?


Two-way binding 是 AngularJS 的招牌菜,但因為底層使用 dirty check 方式,只要頁面 two-way binding 用得過多,效率就會明顯變慢,因此 ReactJS 改用 one-way binding ,在效能上完全打趴 AngularJS。

在 Angular 2 一開始時,由於 AngularJS 的經驗與 ReatJS的影響,的確聽到不少反對再用 Two-way binding 的聲音,不過深入了解之後,會發現在 Angular 的[(ngModel)] 雖然表面上看起來是 two-way binding,但其實內部用的依然是 one-way binding,也就是 [(ngModel)] 只是個 syntax sugar 而已,因此可以放心大膽使用 [(ngModel)],不用再擔心效能問題。

Conclusion


  • ngModel 在實務上非常好用,語法也很精簡,但 Banana in a Box 語法很特殊,一般人都是背下來,經由本文的推導,應該更能體會 [()] 語法的由來。
  • ngModel 雖然表面上看起來是 two-way binding,但底層用的仍然是 one-way binding,因此沒有效能上的問題,請放心使用。

Reference


Angular, Two-way binding
Angular, NgModel
Throughtram, Two-Way Data Binding in Angular
CK’s Notepad, [Angular] Two-way Binding 的運作方式

2017-08-15