序
任何用angular开发的研发人员应该都对rxjs非常熟悉了。实际上框架本身也用rxjs和它的一些概念去进行构建。但它的意义远远不止这些,事实上,我们可以使用rxjs和observable流去写出更多且更好可读性的代码,还可以去减少我们很多的代码量。
使用RXJS去减少我们组件的状态
angular中的每件事都是围绕着组件的状态以及如何将其投影到ui中展开的。有很多时候我们都可以使用流去代表视图中数据。尤其是当处理表单和其他变化很大的内容时,它特别有用。
- 当我们有一个新任务时,我们将考虑状态将如何改变
- 我们在组件中存储新的状态(新属性,嵌套对象等)
- 我们设计新方法来封装新状态如何变化的方式
- 在我们的模板中编写复杂的逻辑
来让我们看下下面这个例子:
@Component({
selector: 'my-component',
template: `
<select [(ngModel)]="selectedUserId" (ngModelChange)="changeUser()">
<option>Select a User</option>
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</select>
<select [(ngModel)]="blackListedUsers" (ngModelChange)="changeUser()" multiple>
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</select>
Allow black listed users <input type="checkbox" [(ngModel)]="allowBlackListedUsers"/>
<button [disabled]="isUserBlackListed && !allowBlackListedUsers">Submit</button>
`,
})
export class MyComponent {
users = [
{name: 'John', id: 1},
{name: 'Andrew', id: 2},
{name: 'Anna', id: 3},
{name: 'Iris', id: 4},
];
blackListedUsers = [];
selectedUserId = null;
isUserBlackListed = false;
allowBlackListedUsers = false;
changeUser() {
this.isUserBlackListed = !!this.blackListedUsers.find(
blackListedUserId => +this.selectedUserId === blackListedUserId
);
}
}
想象一下我们有一个页面,其中有一个下拉选择框,我们从中可以选择用户(里面的选项通常是用ngfor循环遍历出来的)。现在有另一个下拉选择框有相同的用户,选将一些用户将其列入黑名单,列入黑名单的用户不允许被提交。还有一个复选框,勾选表明允许将某如列入黑名单并提交他们(因此哪怕被勾选的用户已经被列入黑名单,该按钮也不会被禁用)。如果用户从第一个下拉框中选择其中一个,则“提交”按钮将被禁用。让我们尝试使用简单的模板驱动表单模型来完全不使用 RxJS 来实现它。
因此,我们有两个数组,三个表单绑定,以及ngModelChange的一个方法来处理状态的更改。后续我们还需要以下逻辑:
- 需要一些状态(isUserBlackListed和allowBlackListedUsers)来存储一些仅在模板中实际需要的数据;
- 需要声明ngmodel绑定的状态
- 写一个对应的方法处理状态的更改
- 在模板中需要对应的禁用逻辑(
[disabled]=”isUserBlackListed && !allowBlackListedUsers”
))
按这样的写法来说,对于初学者,遵循应用程序的逻辑会变得更加困难。例如,如果我是阅读此代码的人,并且我发现某个按钮有时会被禁用,我将执行以下步骤:
- 找到 [disabled] 绑定,可以看到它绑定了两个属性,isUserBlackListed 和allowBlackListedUsers;
- 在组件代码中查找,发现只是一些从 false 开始的基本属性;
- 然后搜索 component.ts 文件以查找对它们的引用;这看起来似乎是理所当然的事情,但是如果我们的属性在多个方法中被引用怎么办?我必须仔细检查所有这些,才能准确找出哪一个会影响禁用按钮;
- 阅读并理解我最终找到的方法。在我们的例子中,这很容易;但在实际开发中,可能就会非常混乱。
另一个缺点是,当出现另一段此类逻辑时,我们最终将在组件代码中增加改变它们的属性和方法。所以我们应该怎么做呢?
更多的响应式思考
现在我们将设计一个简单的三步思考计划。尝试解决同样的问题,但现在使用 Reactive Forms 和 RxJS,我们将执行以下操作:
- 了解状态的哪一部分影响 UI 并使其成为 Observable 流;
- 使用 RxJS 运算符执行计算并导出要在 UI 中使用的最终状态
- 使用异步管道将计算结果放入模板中
import { FormControl } from '@angular/forms';
import { combineLatest } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
@Component({
selector: 'my-app',
template: `
<select [formControl]="selectedUserId">
<option>Select a User</option>
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</select>
<select [formControl]="blackListedUsers" multiple>
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</select>
Allow black listed users <input type="checkbox" [formControl]="allowBlackListedUsers"/>
<button [disabled]="isDisabled$ | async">Submit</button>
`,
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
users = [
{name: 'John', id: 1},
{name: 'Andrew', id: 2},
{name: 'Anna', id: 3},
{name: 'Iris', id: 4},
];
blackListedUsers = new FormControl([]);
selectedUserId = new FormControl(null);
allowBlackListedUsers = new FormControl(false);
isDisabled$ = combineLatest([
this.allowBlackListedUsers.valueChanges.pipe(startWith(false)),
this.blackListedUsers.valueChanges.pipe(startWith([])),
this.selectedUserId.valueChanges.pipe(startWith(null), map(id => +id)),
]).pipe(
map(
([allowBlackListed, blackList, selected]) => !allowBlackListed && blackList.includes(selected),
),
)
}
正如你所看到的,我们实现了一个属性,它是一个 Observable,来处理一些 UI。它使用combineLatest组合了三个表单控件的输出,然后使用它们的组合输出来导出布尔状态。
(注:我们使用 startWith 是因为在 Angular formControl.valueChanges 中,直到用户通过 UI 控件手动更改它,或者强制地通过 setValue 更改它时,Control.valueChanges 才会开始发出,并且在所有源 Observables 至少发出一次之前,combineLatest 不会触发;所以我们需要使用startwith让它们全部在最开始发出默认值。)
现在,当我读到这个组件的模板并思考这个按钮什么时候被禁用时,我将执行以下步骤:
- 只关注isDisabled | async” 绑定末尾的美元符号会立即表明它是一个 Observable;
- 转到该属性定义并查看它是三个数据源的组合;
- 查看数据如何映射到布尔值
isDisabled 及其运算符的定义内,而不是其他地方。你可能认为我们需要取消订阅 Observable?但事实上我们不需要——异步管道为我们做到了这一点。
总结
本文样例在经过rxjs进行重构后,有了以下改变:
- 更容易搜索和找到对应的代码
- 更加简洁;相互影响的逻辑片段被收集在一个地方,而不是分散在整个组件中
- 声明式而非命令式
rxjs是一个强大的工具。它有很多概念和技巧,可以用来使我们的 Angular 代码更好、更易读、更容易推理、更容易理解。