在 Rx--隱藏在Angular 2.x中利劍 一文中我們已經(jīng)初步的了解了 Rx 和 Rx 在 Angular 的應(yīng)用韩肝。 今天我們一起通過(guò)一個(gè)具體的例子來(lái)理解響應(yīng)式編程設(shè)計(jì)的思路。最后會(huì)看看剛剛發(fā)布的 Angular 4 的新特性給響應(yīng)式編程帶來(lái)了什么新鮮的元素迷守。
為什么要做響應(yīng)式編程?
我給出的答案很簡(jiǎn)單:響應(yīng)式編程可以讓你把程序邏輯想的很清楚檩禾。為什么這么說(shuō)呢挂签?讓我們先來(lái)看一個(gè)小例子,比如我們有這樣一個(gè)需求盼产,在生日的控件之前添加一個(gè)年齡的選擇饵婆,用以輔助生日的輸入。雖然很變態(tài)戏售,其實(shí)直接輸入趕腳比這種方式快啊侨核,但真的有客戶提出過(guò)這種需求,不管怎樣我們來(lái)看一下好了灌灾。
首先分析一下需求:
- 年齡可以按歲芹关、月、天為單位紧卒。
- 其中如果年齡小于等于3個(gè)月侥衬,按天為單位,如果小于等于2歲按月為單位跑芳,其余情況按歲為單位轴总。其實(shí)就是考慮幼兒的情況啦。
- 填年齡時(shí)博个,出生日期隨之變化怀樟,因?yàn)闊o(wú)法精確,所以只需精確到選擇的單位即可盆佣。
如果按傳統(tǒng)方式編程的話往堡,我們可能需要在年齡和年齡單位的兩個(gè)處理輸入改變的 event handler
去對(duì)數(shù)據(jù)進(jìn)行處理,具體我們就不展開了共耍。我們來(lái)看一下用響應(yīng)式編程如何處理這個(gè)邏輯虑灰。
理解 Rx 的關(guān)鍵是要把任何變化想象成數(shù)據(jù)流,數(shù)據(jù)流分為幾種:
- 永遠(yuǎn)不會(huì)結(jié)束的
- 有限次的痹兜,比如執(zhí)行若干次結(jié)束的(包括只發(fā)生一次的)
- 當(dāng)然還有一些特殊的穆咐,比如永遠(yuǎn)不會(huì)發(fā)生的(這個(gè)是為了解決某些特定場(chǎng)景問(wèn)題存在的)
這么說(shuō)好像比較抽象,那么還是回到例子來(lái)看這個(gè)問(wèn)題字旭。就這個(gè)需求來(lái)看的話对湃,年齡和年齡單位這兩個(gè)數(shù)據(jù)要一起來(lái)考慮,
上圖中(由于太懶遗淳,后面的合并虛線就沒(méi)有畫了)拍柒,上面兩個(gè)流為原始數(shù)據(jù)流,一個(gè)是年齡的數(shù)據(jù)流屈暗,每次更改年齡數(shù)時(shí)拆讯,這個(gè)數(shù)據(jù)流就產(chǎn)生一個(gè)數(shù)據(jù):比如一開始初始值為 33剧包,我們刪掉個(gè)位數(shù)的 3,這時(shí)由于其變化往果,產(chǎn)生第二個(gè)值 3 (原十位的3)疆液,然后我們添加了5,新值變成35陕贮,因此流中的第三個(gè)數(shù)據(jù)是35堕油,以此類推。另一個(gè)數(shù)據(jù)流反映了年齡單位的變化肮之,按照“歲-月-歲-天”的次序產(chǎn)生新的數(shù)據(jù)掉缺。一個(gè)人的最終的年齡是通過(guò)年齡值和年齡單位聯(lián)合確定的,這也就是說(shuō)我們需要對(duì)這兩個(gè)流做合并計(jì)算戈擒。
那么選擇什么樣的合并方式呢眶明?其實(shí)我們需要的是任何一個(gè)流的值變化的時(shí)候,新的合并流都應(yīng)該有一個(gè)對(duì)應(yīng)數(shù)據(jù)筐高,這個(gè)數(shù)據(jù)包括剛剛變化的那個(gè)值和另一個(gè)流中最新的值搜囱。比如:如果年齡數(shù)據(jù)從 33 刪掉個(gè)位變成 3,此時(shí)我們沒(méi)有改變年齡單位柑土,合并流中的新數(shù)據(jù)應(yīng)該是 3歲
蜀肘。接下來(lái)我們改變單位為 月
,那這時(shí)候年齡數(shù)據(jù)的最新值仍然是 3 稽屏,所以新流的數(shù)據(jù)應(yīng)為 3月
等等以此類推扮宠。
這樣的一種合并方式在 Rx 中專門有一個(gè)操作符來(lái)處理,那就是 combineLatest
狐榔。如果我們使用 age$
代表年齡數(shù)據(jù)流(那個(gè) $
代表 Stream -- 流的意思坛增,約定俗成的寫法,不強(qiáng)制要求)薄腻,用 ageUnit$
代表年齡單位數(shù)據(jù)流的話收捣,我們可以寫出如下的合并邏輯,為了簡(jiǎn)化問(wèn)題被廓,我們這里合并后都使用 天
作為單位:
// 這里前面兩個(gè)參數(shù)都是參與合并的數(shù)據(jù)流坏晦,第三個(gè)是個(gè)處理函數(shù)
// 這個(gè)處理函數(shù)接受兩個(gè)流中的最新數(shù)據(jù)萝玷,然后經(jīng)過(guò)運(yùn)算輸出新值
this.computed$ = Observable.combineLatest(age$, ageUnit$, (a, u)=>{
// 非法數(shù)字就都按初始值處理嫁乘,這里就簡(jiǎn)單粗暴了
if(a === undefined || a <= 0 ) return initialAge;
// 全部轉(zhuǎn)化為天數(shù)
switch (parseInt(u)) {
case AgeUnit.Day.valueOf():
return a;
case AgeUnit.Month.valueOf():
return a * 30;
case AgeUnit.Year.valueOf():
default:
// 別問(wèn)我閏年大小月啥的,只是個(gè)例子而已
return a * 365;
}
})
合并之后呢球碉,由于我們最終需要向生日那個(gè)輸入框中寫入一個(gè)日期蜓斧,而我們合并之后的流給出的是按天數(shù)計(jì)算的年齡,所以這里顯然需要一個(gè)轉(zhuǎn)換睁冬。
在 Rx 中這種數(shù)據(jù)的轉(zhuǎn)換再容易不過(guò)了挎春,最常用的一個(gè)就是 map
轉(zhuǎn)換操作符看疙,接著上面的代碼繼續(xù)來(lái)一個(gè) map
函數(shù),這里使用了 momentjs
的按當(dāng)前日期減去剛剛的以天數(shù)為單位的年齡值直奋,就得到一個(gè)大概估算的出生日期能庆。
.map(a => {
const date = moment().subtract(a, 'days').format('YYYY-MM-DD');
return date;
});
但是到這里,你會(huì)發(fā)現(xiàn)我們還沒(méi)有定義兩個(gè)原始數(shù)據(jù)流呢脚线,別急搁胆,留到后面是為了引出 Angular 對(duì)于 Rx 的良好支持。
響應(yīng)式表單中的 Rx
Angular 的表單處理非常強(qiáng)大邮绿,有模版驅(qū)動(dòng)的表單和響應(yīng)式表單兩類渠旁,兩種表單各有千秋,在不同場(chǎng)合可以分別使用船逮,甚至混合使用顾腊,但這里就不展開了。我們這里使用了響應(yīng)式表單挖胃,也非常簡(jiǎn)單杂靶,就是一個(gè) form
里面 3 個(gè)控件,這里我采用了官方的 Material 控件酱鸭,如果你覺(jué)得不爽伪煤,可以直接用基礎(chǔ)的 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 中處理響應(yīng)式表單只有 3 個(gè)步驟:
- 在組件的 HTML 模版中給要處理的控件加上
formControlName="blablabla"
-
form
標(biāo)簽中添加[formGroup]="xxx"
指令凛辣,這個(gè)xxx
就是你在組件中聲明的FormGroup
類型的成員變量:比如下面代碼中的form: FormGroup;
- 在組件的構(gòu)造函數(shù)中取得
FormBuilder
后(比如下面代碼中的constructor(private fb: FormBuilder) { }
)抱既,用FormBuilder
構(gòu)造表單控件數(shù)組并賦值給剛才的類型為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;
}
}
現(xiàn)在這個(gè)表單就建立好了扁誓,但你可能會(huì)問(wèn)防泵,這也沒(méi)看出來(lái)響應(yīng)式啊,別急蝗敢,接下來(lái)我們就要看看它的響應(yīng)式支持了捷泞。我們?cè)倩氐揭婚_始的小題目,我們的兩個(gè)原始數(shù)據(jù)流:age$
和 ageUnit$
怎么構(gòu)建寿谴?這兩個(gè)數(shù)據(jù)流其實(shí)是來(lái)自于兩個(gè)控件的值的變化锁右,而響應(yīng)式表單獲取值的變化是非常簡(jiǎn)單的就一行:
this.form.controls['age'].valueChanges
上面這行代碼的意思是從表單的控件數(shù)組中取得 formControlName
為 age
的這個(gè)控件然后監(jiān)聽(tīng)其值的變化。這個(gè) valueChanges
返回的其實(shí)就是一個(gè) Observable
讶泰,見(jiàn)下面的 TypeScript 定義:
/**
* Emits an event every time the value of the control changes, in
* the UI or programmatically.
*/
readonly valueChanges: Observable<any>;
既然我們得到了這個(gè)原始數(shù)據(jù)流咏瑟,剩下的工作就比較簡(jiǎn)單了。但我們可能需要對(duì)這個(gè)原始數(shù)據(jù)流再做點(diǎn)處理痪署。首先码泞,我們并不希望每次改這個(gè)值都去監(jiān)聽(tīng),因?yàn)檩斎胧且粋€(gè)連續(xù)事件狼犯,每一次按鍵都監(jiān)聽(tīng)是不太劃算的余寥。這就需要一個(gè)濾波器的處理 .debounceTime(500)
领铐,我們不去處理 500 毫秒內(nèi)的變化,而是等待其輸入停頓時(shí)再發(fā)送數(shù)據(jù)宋舷。第二绪撵,如果用戶采用了拷貝粘貼的方式,我們希望同樣的數(shù)據(jù)不重復(fù)發(fā)送祝蝠,所以濾掉相同的數(shù)據(jù)莲兢。最后,我們采用 startWith
給這個(gè)流一個(gè)初始值续膳,這是由于如果一開始我們什么都不做改艇,兩個(gè)流就都沒(méi)有數(shù)據(jù);或者只改變其中一個(gè)坟岔,另一個(gè)由于一直沒(méi)有變就不會(huì)產(chǎn)生數(shù)據(jù)谒兄,這樣的話,合并流也不會(huì)有數(shù)據(jù)社付。
// 省略其它引入
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 管道
到目前為止承疲,我們還沒(méi)有進(jìn)行對(duì) Observable 的訂閱,如果不訂閱的話鸥咖,寫的再漂亮的語(yǔ)句也不會(huì)執(zhí)行的燕鸽。按常規(guī)套路來(lái)講伊磺,我們得聲明 Subscription 對(duì)象昙篙,因?yàn)?Observable 是一直監(jiān)聽(tīng)的柳弄,即使頁(yè)面銷毀矛渴,它也還在,這會(huì)造成內(nèi)存泄漏绢彤。所以描馅,我們需要再頁(yè)面銷毀(ngOnDestroy
中)的適合取消訂閱营勤。 需要訂閱的 Observable
少的時(shí)候還好富弦,一旦多起來(lái)沟娱,處理時(shí)也挺麻煩,像下面的代碼那樣腕柜。
// 省略其它引入
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 提供了對(duì)于響應(yīng)式編程非常友好的設(shè)計(jì),我們完全可以不在代碼中做訂閱或取消訂閱的動(dòng)作盏缤。那么問(wèn)題來(lái)了砰蠢,不訂閱的話,值怎么獲得呢蛾找?答案是 Async 管道娩脾。Async 會(huì)在組件初始化時(shí)自動(dòng)的訂閱以及在組件銷毀時(shí)自動(dòng)取消訂閱,太爽了打毛。因此柿赊,我們可以刪掉上面的代碼了,然后在組件模版中給生日的那個(gè) input
添加一個(gè)指令 [value]="computed$ | async"
幻枉,這就是說(shuō)該 input
的 value 就是 computed$
訂閱后的值碰声,那么 | async
是說(shuō) computed$
是一個(gè) Observable,請(qǐng)對(duì)他采用異步處理熬甫,即初始化時(shí)自動(dòng)的訂閱以及在組件銷毀時(shí)自動(dòng)取消訂閱胰挑。
<input mdInput
formControlName="dateOfBirth"
// 省略其它屬性
[value]="computed$ | async"
/>
對(duì)于響應(yīng)式編程方式的思考
上面的例子,我不知道大家發(fā)現(xiàn)沒(méi)有椿肩,當(dāng)然 Rx 提供了好多方便的操作符瞻颂。但更重要的是,寫 Rx 的時(shí)候郑象,我們需要對(duì)流程理解的足夠清晰贡这,或者說(shuō) Rx 逼著我們對(duì)流程反復(fù)梳理。其實(shí)有的時(shí)候厂榛,寫 Rx 不一定很快盖矫,但一旦業(yè)務(wù)梳理清楚了,接下來(lái)就是幾行代碼的事情击奶。如果你有時(shí)候覺(jué)得用現(xiàn)有的 Rx 操作符寫不出辈双,那多半是你的對(duì)需求中涉及的數(shù)據(jù)流的關(guān)系沒(méi)有弄清楚。
Angular 4 中的 NgIf 的改進(jìn)
Angular 4 中的 ngIf
現(xiàn)在可以攜帶 else
了柜砾,如果你曾經(jīng)使用過(guò) Angular 就知道湃望,原來(lái)我們是得寫兩個(gè) ngIf
來(lái)完成類似的功能的。這個(gè) else
可以攜帶一個(gè)模版的引用痰驱。比如下面例子中:如果用戶登錄成功顯示用戶名喜爷,否則顯示登錄鏈接。
<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>
另一個(gè)改進(jìn)是 ngIf
中現(xiàn)在可以將評(píng)估表達(dá)式的結(jié)果賦值給一個(gè)變量萄唇,好處是什么呢檩帐?可以讓你少寫很多 (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>
有問(wèn)題的童鞋可以加入我的小密圈討論: http://t.xiaomiquan.com/jayRnaQ (該鏈接7天內(nèi)(5月14日前)有效)
好久沒(méi)寫 Angular 了,希望后面會(huì)有時(shí)間多寫一些另萤。另外湃密,我的 《Angular 從零到一》出版了,本文出自第 8 章部分內(nèi)容四敞,下面是書籍的內(nèi)容簡(jiǎn)介:
本書系統(tǒng)介紹Angular的基礎(chǔ)知識(shí)與開發(fā)技巧泛源,可幫助前端開發(fā)者快速入門。共有9章忿危,第1章介紹Angular的基本概念达箍,第2~7章從零開始搭建一個(gè)待辦事項(xiàng)應(yīng)用,然后逐步增加功能铺厨,如增加登錄驗(yàn)證缎玫、將應(yīng)用模塊化硬纤、多用戶版本的實(shí)現(xiàn)、使用第三方樣式庫(kù)赃磨、動(dòng)態(tài)效果制作等筝家。第8章介紹響應(yīng)式編程的概念和Rx在Angular中的應(yīng)用。第9章介紹在React中非常流行的Redux狀態(tài)管理機(jī)制邻辉,這種機(jī)制的引入可以讓代碼和邏輯隔離得更好溪王,在團(tuán)隊(duì)工作中強(qiáng)烈建議采用這種方案。本書不僅講解Angular的基本概念和最佳實(shí)踐值骇,而且分享了作者解決問(wèn)題的過(guò)程和邏輯莹菱,講解細(xì)膩,風(fēng)趣幽默吱瘩,適合有面向?qū)ο缶幊袒A(chǔ)的讀者閱讀道伟。
歡迎大家圍觀、訂購(gòu)搅裙、提出寶貴意見(jiàn)皱卓。
京東鏈接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0