在现实生活中,感知数据变化的方式有两种,第一种是**数据监听**,第二种是**观察者模式**。
以在京东购物为例,假设你最有购买一块CPU的需求,由于口袋不太满,所以想在搞活动的时候购买。满足此需求有两种实现的方式。
第一种:我们可以每天睡前打开京东看看价格。如果有一天CPU的价格达到了自己心中的预期,便可以购买了。这就是最典型的数据监听。
第二种:我们可以在对应的商品上点击降价提醒按钮,并输入自己心中的一个预期价格。此后,一旦CPU降到了我们预期的价格,我们则会收到一个降价的通知。而这便是我们说的**观察者模式**。
> 如果**观察者模式**不太容易理解,也可以记忆为**订阅者模式**、**关注者模式**,想想各种求订阅、求关注的情景,相信这种模式便不难理解了吧。
如果你也是我这样的懒人,相信也一定会选择第二种模式。这种模式可以使我们能够再懒一些,对于计算机而言,这种懒对应的便是:占用资源更低、效率更高。
## 伟大的Angular
伟大的Agnular并不仅仅是一个前端的框架,它还是先进编程思想的传播者!这种感受会随着我们接触Angular的时间而逐步加深。
## 响应式表单
响应式表单在数据监听方面实现了**观察者模式**,在此模式的支持下,数据变化时我们可以得到一个通知。接下来我们使用响应式表单对教师选择组件进行改造。
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts
@@ -1,6 +1,7 @@
import {Component, OnInit, EventEmitter, Output} from '@angular/core';
import {Teacher} from '../../entity/teacher';
import {HttpClient} from '@angular/common/http';
+import {FormControl} from '@angular/forms';
@Component({
@@ -10,7 +11,7 @@ import {HttpClient} from '@angular/common/http';
})
export class KlassSelectComponent implements OnInit {
teachers = new Array<Teacher>();
- teacherId: number | undefined;
+ teacherId = new FormControl();
@Output()
beChange = new EventEmitter<number>();
@@ -23,10 +24,4 @@ export class KlassSelectComponent implements OnInit {
this.httpClient.get<Array<Teacher>>('teacher')
.subscribe(teachers => this.teachers = teachers);
}
-
- onChange(): void {
- console.log('change called');
- console.log(this.teacherId);
- this.beChange.emit(this.teacherId);
- }
}
```
如上所示:使用对象`FormControl`来替代原来的`number`类型,并删除原来的`onChange`方法。然后对V层进行如下改造:
```html
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.html
@@ -1,7 +1,5 @@
<select id="teacher" class="form-control"
- name="teacher"
- [(ngModel)]="teacherId"
- (change)="onChange()">
+ [formControl]="teacherId">
<option *ngFor="let teacher of teachers" [ngValue]="teacher.id">
{{teacher.name}}
</option>
```
`formControl`并不要求表单项必须有`name`,在此我们一并删除了`name`属性。接下来向班级模块中添加响应式表单所在的模块`ReactiveFormsModule`:
```typescript
+++ b/first-app/src/app/clazz/clazz.module.ts
@@ -1,7 +1,7 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AddComponent} from './add/add.component';
-import {FormsModule} from '@angular/forms';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { KlassSelectComponent } from './klass-select/klass-select.component';
@@ -9,7 +9,8 @@ import { KlassSelectComponent } from './klass-select/klass-select.component';
declarations: [AddComponent, KlassSelectComponent],
imports: [
CommonModule,
- FormsModule
+ FormsModule,
+ ReactiveFormsModule
]
})
export class ClazzModule {
```
最后,向单元测试的动态测试模块中也添加`ReactiveFormsModule`模块:
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts
-import {FormsModule} from '@angular/forms';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
imports: [
HttpClientModule,
- FormsModule
+ FormsModule,
+ ReactiveFormsModule
],
```
### 测试
启动单元并查看效果,查看控制台是否有报错信息:
![image-20210324084043748](https://img.kancloud.cn/ff/ab/ffab5f83c4049bc5e37438562aa276db_964x188.png)
小结:直接在表单中使用`[(ngModel)]`虽然便捷,但却不够**正综**。Angular的前身AngularJS尝经广泛的使用了这种`ngModel`数据绑定方式,当年也获取了很大的成功。但这种方式也会带来不小的开销,当项目稍大的时,便会暴露出性能上的弊端。Angular中启用了全新的**响应式表单**来替换原`ngModel`的绑定方式,但可能是出于照顾使用AngularJS的历史用户,使他们在升级时能够减小一点点负担,所以在新的Angular中仍然支持了这种模式。
## 订阅数据变化
下面,我们展示如何来**关注**`teacherId`这个`FormControl`以成为它的粉丝,这样一来,当它有新的动态时便会给我们发送通知了。
```typescript
+++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts
@@ -20,6 +20,9 @@ export class KlassSelectComponent implements OnInit {
}
ngOnInit(): void {
+ // 关注teacherId
+ this.teacherId.valueChanges
+ .subscribe((data: number) => this.beChange.emit(data));
// 获取所有教师
this.httpClient.get<Array<Teacher>>('teacher')
.subscribe(teachers => this.teachers = teachers);
```
`valueChanges`是`FormControl`提供的一个可对其进行关注的属性,我们只需要调用`valueChanges`上的`subscribe`方法,便完成了对数据的**关注**。当有新的数据时,我们传入的订阅方法将被执行,数据成功由beChange弹射器弹出给父组件。
## 测试
我们像以往的测试一样选择教师:
![image-20210324084043748](https://img.kancloud.cn/ff/ab/ffab5f83c4049bc5e37438562aa276db_964x188.png)
选择的教师变更后,接收到了弹出的数据。
![image-20210325161027648](https://img.kancloud.cn/8e/15/8e150d3277dc1c338d1077b359eba282_1260x268.png)
## 总结
本节中,我们使用响应式表单的`FormControl`对象来替换了原`number`类型。在与V层的绑定上`FormControl`并不要求表单项必须存在`name`属性。
在监听数据变化方法,响应式表单使用了观察者模式。 我们虽然还没有感受到这种模式给我们带来的好处。但相对于原`change()`方法,最少在代码量上有所降低。
在后续的章节中,我们会千篇一律的使用响应表单,相信你会逐步的感受它的魅力。
| 名称 | 链接 |
| ------------ | ------------------------------------------------------------ |
| 响应式表单 | [https://angular.cn/guide/reactive-forms#reactive-forms](https://angular.cn/guide/reactive-forms#reactive-forms) |
| valueChanges | [https://angular.cn/api/forms/AbstractControl#valueChanges](https://angular.cn/api/forms/AbstractControl#valueChanges) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.2.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.2.2.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结