Angular 中的響應式編程 -- 淺淡 Rx 的流式思惟

第一節:初識Angular-CLI
第二節:登陸組件的構建
第三節:創建一個待辦事項應用
第四節:進化!模塊化你的應用
第五節:多用戶版本的待辦事項應用
第六節:使用第三方樣式庫及模塊優化用
第七節:給組件帶來活力
Rx--隱藏在 Angular 中的利劍
Redux你的 Angular 應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)javascript

Rx -- 隱藏在 Angular 中的利劍 一文中咱們已經初步的瞭解了 Rx 和 Rx 在 Angular 的應用。 今天咱們一塊兒經過一個具體的例子來理解響應式編程設計的思路。最後會看看剛剛發佈的 Angular 4 的新特性給響應式編程帶來了什麼新鮮的元素。css

爲何要作響應式編程?

我給出的答案很簡單:響應式編程可讓你把程序邏輯想的很清楚。爲何這麼說呢?讓咱們先來看一個小例子,好比咱們有這樣一個需求,在生日的控件以前添加一個年齡的選擇,用以輔助生日的輸入。雖然很變態,其實直接輸入趕腳比這種方式快啊,但真的有客戶提出過這種需求,無論怎樣咱們來看一下好了。html

有年齡和單位選擇的日期輸入

首先分析一下需求:java

  • 年齡能夠按歲、月、天爲單位。
  • 其中若是年齡小於等於3個月,按天爲單位,若是小於等於2歲按月爲單位,其他狀況按歲爲單位。其實就是考慮幼兒的狀況啦。
  • 填年齡時,出生日期隨之變化,由於沒法精確,因此只需精確到選擇的單位便可。

若是按傳統方式編程的話,咱們可能須要在年齡和年齡單位的兩個處理輸入改變的 event handler 去對數據進行處理,具體咱們就不展開了。咱們來看一下用響應式編程如何處理這個邏輯。react

理解 Rx 的關鍵是要把任何變化想象成數據流,數據流分爲幾種:編程

  1. 永遠不會結束的
  2. 有限次的,好比執行若干次結束的(包括只發生一次的)
  3. 固然還有一些特殊的,好比永遠不會發生的(這個是爲了解決某些特定場景問題存在的)

這麼說好像比較抽象,那麼仍是回到例子來看這個問題。就這個需求來看的話,年齡和年齡單位這兩個數據要一塊兒來考慮,數組

數據流的合併

上圖中(因爲太懶,後面的合併虛線就沒有畫了),上面兩個流爲原始數據流,一個是年齡的數據流,每次更改年齡數時,這個數據流就產生一個數據:好比一開始初始值爲 33,咱們刪掉個位數的 3,這時因爲其變化,產生第二個值 3 (原十位的3),而後咱們添加了5,新值變成35,所以流中的第三個數據是35,以此類推。另外一個數據流反映了年齡單位的變化,按照「歲-月-歲-天」的次序產生新的數據。一我的的最終的年齡是經過年齡值和年齡單位聯合肯定的,這也就是說咱們須要對這兩個流作合併計算。app

那麼選擇什麼樣的合併方式呢?其實咱們須要的是任何一個流的值變化的時候,新的合併流都應該有一個對應數據,這個數據包括剛剛變化的那個值和另外一個流中最新的值。好比:若是年齡數據從 33 刪掉個位變成 3,此時咱們沒有改變年齡單位,合併流中的新數據應該是 3歲 。接下來咱們改變單位爲 ,那這時候年齡數據的最新值仍然是 3 ,因此新流的數據應爲 3月等等以此類推。dom

這樣的一種合併方式在 Rx 中專門有一個操做符來處理,那就是 combineLatest。若是咱們使用 age$ 表明年齡數據流(那個 $ 表明 Stream -- 流的意思,約定俗成的寫法,不強制要求),用 ageUnit$ 表明年齡單位數據流的話,咱們能夠寫出以下的合併邏輯,爲了簡化問題,咱們這裏合併後都使用 做爲單位:異步

// 這裏前面兩個參數都是參與合併的數據流,第三個是個處理函數
// 這個處理函數接受兩個流中的最新數據,而後通過運算輸出新值
this.computed$ = Observable.combineLatest(age$, ageUnit$, (a, u)=>{
      // 非法數字就都按初始值處理,這裏就簡單粗暴了
      if(a === undefined || a <= 0 ) return initialAge;
      // 所有轉化爲天數
      switch (parseInt(u)) {
        case AgeUnit.Day.valueOf():
          return a;
        case AgeUnit.Month.valueOf():
          return a * 30;
        case AgeUnit.Year.valueOf():
        default:
          // 別問我閏年大小月啥的,只是個例子而已
          return a * 365; 
      }
    })複製代碼

合併以後呢,因爲咱們最終須要向生日那個輸入框中寫入一個日期,而咱們合併以後的流給出的是按天數計算的年齡,因此這裏顯然須要一個轉換。

在 Rx 中這種數據的轉換再容易不過了,最經常使用的一個就是 map 轉換操做符,接着上面的代碼繼續來一個 map 函數,這裏使用了 momentjs 的按當前日期減去剛剛的以天數爲單位的年齡值,就獲得一個大概估算的出生日期。

.map(a => {
      const date = moment().subtract(a, 'days').format('YYYY-MM-DD');
      return date;
    });複製代碼

可是到這裏,你會發現咱們尚未定義兩個原始數據流呢,別急,留到後面是爲了引出 Angular 對於 Rx 的良好支持。

響應式表單中的 Rx

Angular 的表單處理很是強大,有模版驅動的表單和響應式表單兩類,兩種表單各有千秋,在不一樣場合能夠分別使用,甚至混合使用,但這裏就不展開了。咱們這裏使用了響應式表單,也很是簡單,就是一個 form 裏面 3 個控件,這裏我採用了官方的 Material 控件,若是你以爲不爽,能夠直接用基礎的 HTML 控件搭配樣式便可。

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <md-input-container align="end">
      <input mdInput formControlName="age" type="number" placeholder="年齡" max="200" min="1" />
  </md-input-container>
  <md-button-toggle-group formControlName="ageUnit">
    <md-button-toggle value="0" ></md-button-toggle>
    <md-button-toggle value="1" ></md-button-toggle>
    <md-button-toggle value="2" ></md-button-toggle>
  </md-button-toggle-group>
  <md-input-container>
      <input mdInput formControlName="dateOfBirth" type="date" placeholder="出生日期" max="2100-12-31" min="1900-01-01" [value]="computed$ | async" />
      <md-hint align="start">YYYY/MM/DD格式輸入</md-hint>
  </md-input-container>
</form>複製代碼

Angular 中處理響應式表單只有 3 個步驟:

  1. 在組件的 HTML 模版中給要處理的控件加上 formControlName="blablabla"
  2. form 標籤中添加 [formGroup]="xxx" 指令,這個 xxx 就是你在組件中聲明的 FormGroup 類型的成員變量:好比下面代碼中的 form: FormGroup;
  3. 在組件的構造函數中取得 FormBuilder 後(好比下面代碼中的 constructor(private fb: FormBuilder) { }),用 FormBuilder 構造表單控件數組並賦值給剛纔的類型爲 FormGroup 的成員變量。
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import { AgeUnit } from '../../domain/entities.interface';
import * as moment from 'moment/moment';

@Component({
  selector: 'app-reactive',
  templateUrl: './reactive.component.html',
  styleUrls: ['./reactive.component.scss']
})
export class ReactiveComponent implements OnInit {
  form: FormGroup;
  computed$: Observable<string>;
  ageSub: Subscription;
  dateOfBirth$: Observable<string>;
  dateOfBirthSub: Subscription;
  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      age: ['', Validators.required],
      ageUnit: ['', Validators.required],
      dateOfBirth: ['', Validators.compose([Validators.required, this.validateDate])]
    });

    const initialAge = 33;
    const initialAgeUnit = AgeUnit.Year;
    this.form.controls['age'].setValue(initialAge);
    this.form.controls['ageUnit'].setValue(initialAgeUnit);
  }

  validateDate(c: FormControl): {[key: string]: any}{
    const result = moment(c.value).isValid 
        && moment(c.value).isBefore()
        && moment(c.value).year()> 1900;
    return {
      "valid": result
    }
  }

  onSubmit() {
    if(!this.form.valid) return;
  }
}複製代碼

如今這個表單就創建好了,但你可能會問,這也沒看出來響應式啊,別急,接下來咱們就要看看它的響應式支持了。咱們再回到一開始的小題目,咱們的兩個原始數據流:age$ageUnit$ 怎麼構建?這兩個數據流實際上是來自於兩個控件的值的變化,而響應式表單獲取值的變化是很是簡單的就一行:

this.form.controls['age'].valueChanges複製代碼

上面這行代碼的意思是從表單的控件數組中取得 formControlNameage 的這個控件而後監聽其值的變化。這個 valueChanges 返回的其實就是一個 Observable ,見下面的 TypeScript 定義:

/** * Emits an event every time the value of the control changes, in * the UI or programmatically. */
readonly valueChanges: Observable<any>;複製代碼

既然咱們獲得了這個原始數據流,剩下的工做就比較簡單了。但咱們可能須要對這個原始數據流再作點處理。首先,咱們並不但願每次改這個值都去監聽,由於輸入是一個連續事件,每一次按鍵都監聽是不太划算的。這就須要一個濾波器的處理 .debounceTime(500),咱們不去處理 500 毫秒內的變化,而是等待其輸入停頓時再發送數據。第二,若是用戶採用了拷貝粘貼的方式,咱們但願一樣的數據不重複發送,因此濾掉相同的數據。最後,咱們採用 startWith 給這個流一個初始值,這是因爲若是一開始咱們什麼都不作,兩個流就都沒有數據;或者只改變其中一個,另外一個因爲一直沒有變就不會產生數據,這樣的話,合併流也不會有數據。

// 省略其它引入
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
// 省略其它部分
const age$ = this.form.controls['age'].valueChanges
      .debounceTime(500)
      .distinctUntilChanged()
      .startWith(initialAge);
const ageUnit$ = this.form.controls['ageUnit'].valueChanges
      .distinctUntilChanged()
      .startWith(initialAgeUnit);複製代碼

Async 管道

到目前爲止,咱們尚未進行對 Observable 的訂閱,若是不訂閱的話,寫的再漂亮的語句也不會執行的。按常規套路來說,咱們得聲明 Subscription 對象,由於 Observable 是一直監聽的,即便頁面銷燬,它也還在,這會形成內存泄漏。因此,咱們須要再頁面銷燬(ngOnDestroy 中)的適合取消訂閱。 須要訂閱的 Observable 少的時候還好,一旦多起來,處理時也挺麻煩,像下面的代碼那樣。

// 省略其它引入
import { Subscription } from 'rxjs/Subscription';
// 省略其它部分
ageSub: Subscription;
// 省略其它部分
this.ageSub = this.computed$.subscribe(date => this.form.controls['dateOfBirth'].setValue(date));
// 省略其它部分
onNgDestroy(){
  if(this.ageSub !== undefined || !this.ageSub.closed)
    this.ageSub.unsubscribe();
}複製代碼

所幸的是,Angular 提供了對於響應式編程很是友好的設計,咱們徹底能夠不在代碼中作訂閱或取消訂閱的動做。那麼問題來了,不訂閱的話,值怎麼得到呢?答案是 Async 管道。Async 會在組件初始化時自動的訂閱以及在組件銷燬時自動取消訂閱,太爽了。所以,咱們能夠刪掉上面的代碼了,而後在組件模版中給生日的那個 input 添加一個指令 [value]="computed$ | async",這就是說該 input 的 value 就是 computed$ 訂閱後的值,那麼 | async 是說 computed$ 是一個 Observable,請對他採用異步處理,即初始化時自動的訂閱以及在組件銷燬時自動取消訂閱。

<input mdInput formControlName="dateOfBirth" // 省略其它屬性 [value]="computed$ | async" />複製代碼

對於響應式編程方式的思考

上面的例子,我不知道你們發現沒有,固然 Rx 提供了好多方便的操做符。但更重要的是,寫 Rx 的時候,咱們須要對流程理解的足夠清晰,或者說 Rx 逼着咱們對流程反覆梳理。其實有的時候,寫 Rx 不必定很快,但一旦業務梳理清楚了,接下來就是幾行代碼的事情。若是你有時候以爲用現有的 Rx 操做符寫不出,那多半是你的對需求中涉及的數據流的關係沒有弄清楚。

Angular 4 中的 NgIf 的改進

Angular 4 中的 ngIf 如今能夠攜帶 else 了,若是你曾經使用過 Angular 就知道,原來咱們是得寫兩個 ngIf 來完成相似的功能的。這個 else 能夠攜帶一個模版的引用。好比下面例子中:若是用戶登陸成功顯示用戶名,不然顯示登陸連接。

<span *ngIf="auth$ else login">
  <a routerLink="/profile">{{(auth$|async).user.name}}</a>
  <a routerLink="/blablabla">{{(auth$|async).visits}}</a>
</span>
<ng-template #login>
  <a routerLink="/login">登陸</a>
</ng-template>複製代碼

另外一個改進是 ngIf 中如今能夠將評估表達式的結果賦值給一個變量,好處是什麼呢?可讓你少寫不少 (auth$|async)

<span *ngIf="auth$ | async as auth else login">
  <a routerLink="/profile">{{auth.user.name}}</a>
  <a routerLink="/blablabla">{{auth.visits}}</a>
</span>
<ng-template #login>
  <a routerLink="/login">登陸</a>
</ng-template>複製代碼

很久沒寫 Angular 了,但願後面會有時間多謝一些。另外,個人 《Angular 從零到一》出版了,歡迎你們圍觀、訂購、提出寶貴意見。

京東連接:item.m.jd.com/product/120…

Angular從零到一
相關文章
相關標籤/搜索