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从零到一
相关文章
相关标签/搜索