上一小节的测试中,发现在进行学生编辑时班级选择组件无法自动选中学生所在的班级。下面来尝试查找并解决该问题。和上节的测试准备工作一致,经过一系列的启动与数据准备。来到学生编辑功能的测试环节:
![](https://img.kancloud.cn/4d/44/4d44a3f441f1138b7ea39be615af5fbc_907x225.png)
点击编辑按钮:
![](https://img.kancloud.cn/71/e6/71e62dc2f5af660765af661ad76373e8_717x109.png)
# 问题猜测
像这种V层的表现与C层的逻辑不相符的问题,首先的解决方法就是追溯数据流。即一些我们认为传入的数据是否成功的被发送了,发送后又是否被成功的接收了,如果数据发送与接收都没有问题,那么实际发送的数据是否与心中预期的数据是一致的。
按着这套逻辑,首先来看数据是否被成功的接收了。因为数据一旦被成功的接收就可以认为数据发送肯定是没有问题的(如果发送不成功,何谈接收成功呢?)。
# 追溯数据流
对于组件而言,查看数据是否成功的被接收只需要在V层中打印要查看的变量即可,比如此时需要查看 班级选择 组件是否成功的接收了班级信息。也就是判断下面的这行代码:
src/app/student/edit/edit.component.html
```
<label>班级:<app-klass-select [klass]="student.klass" (selected)="onSelectKlass($event)"></app-klass-select></label>
```
上述代码中=`[klass]="student.klass"`是负责传值的语句。如果数据成功的传入班级选择组件,则可以在班级选择组件的V层中直接打印该变量即可。
src/app/student/klass-select/klass-select.component.html
```
<pre>{{klass | json}}</pre> ✚
<app-select
[url]="url"
(selected)="onSelected($event)"
[object]="klass"></app-select>
```
效果如下:
![](https://img.kancloud.cn/5b/a1/5ba1f8c1275cb29eb7a6223a70be4451_829x394.png)
上图成功的打印出了klass变量的值,也就是说班级值已被成功的传入了班级选择组件。确认传入成功后,删除刚刚添加的测试代码,继续往下追踪数据源。
发现本组件传入的klass组件又被传入到`<app-select`组件。是否该组件的传值未成功的呢?打开组件相应的V层,加入测试代码:
src/app/core/select/select.component.html
```
<pre>{{object | json}}</pre> ✚
<select id="objectSelect" [formControl]="objectSelect" (change)="onChange()" [compareWith]="compareFn">
<option *ngFor="let object of objects" [ngValue]="object">
{{object.name}}
</option>
</select>
```
同样能打印出数据:
![](https://img.kancloud.cn/5b/a1/5ba1f8c1275cb29eb7a6223a70be4451_829x394.png)
数据传到此便结束了,可以确认数据在传输过程中是成功的,未自动选择的原因莫非是:传入的班级并不存在于选择组件的班级列表中?于是,继续在V层中打印班级列表:
src/app/core/select/select.component.html
```
<pre>{{object | json}}</pre>
<select id="objectSelect" [formControl]="objectSelect" (change)="onChange()" [compareWith]="compareFn">
<option *ngFor="let object of objects" [ngValue]="object">
{{object.name}}
</option>
</select>
<pre>{{objects | json}}</pre> ✚
```
![](https://img.kancloud.cn/db/f7/dbf7aa9daa23adc2974df58f2d894995_564x691.png)
但仔细的对比上述两个值却没有发现任何问题,数据完全的一致。至此诡异的问题出现了:1. 班级选择组件已经成功的接收了班级的值。 2. 班级列表组件的班级列表中也实实在在的存在该班级。但是为什么就没有自动的为我们选中该班级呢?莫非是前面针对选择组件的单元测试有问题导致了该组件根本就没有此功能?还是说angular本身出现了问题?
# 异步处理
其实谁都没问题。刚刚在V层中排查后并未发现问题。这是追踪数据源最简单有效的方法,但有些时候也并不见得适用,比如当前。除了在V层中排查外,还可以使用在C层中控制台打印数据的方法来进行排查。
按此思路打开选择组件的C层:
src/app/core/select/select.component.ts
```
/**
* 获取所有的对象,并传给V层
*/
ngOnInit() {
console.log(this.object); ✚
this.objectSelect = new FormControl(this.object); ➊
this.httpClient.get(this.url)
.subscribe((objects: Array<Select>) => {
this.objects = objects;
});
}
```
却得到了这个结果:
![](https://img.kancloud.cn/e9/b3/e9b3ea99b14752f88934678766429d77_841x138.png)
被打印的变量显示未定义。此时面临的问题是:在C层打印变量,显示变量未定义;在V层打印同名变量,却能打印中相应的值。这明显是一对矛盾。V层的值是由C层传入的,如果C层显示未定义,那么V层打印的结果也应该是未定义才对。其实真正引发此问题的原因是:异步。
C层打印的没有错,因为在C层打印的瞬间,变量object的确就是没有值。V层显示的也没错,因为V层会时时的打印C层变量的最新值。也就是说:程序执行的过程大概是这样:
* ➊ 组件初始化,C层接收的object的值为undefined
* ➊ 组件使用值为undefined的object初始化objectSelect.
* 组件获取班级列表,由于没有班级与值值为undefined的objectSelect是相等的,所以不选中任一班级。
* V层打印值为undefined的object
* 在很短的时间内,组件接收到了新的object的, V层显示最新的object情况。
这就与当前看到的情况相符合了。选择组件未自动选择是由于初始了undefined的objectSelect。V层显示了非undefined的klass是由于在经过了很短的我们还觉察不到的时间后,就获取到了有值的object。而此时组件已经初始化完毕,以前为undefined的objectSelect并没有因接收到了新的非undefined的klass而改变。这就是未何数据流都正确但却未自动选中班级的原因所在。
解决办法:将接收到新的object(klass)时,使用最新接收的object(klass)重新初始化objectSelect。
src/app/core/select/select.component.ts
```
@Output() selected = new EventEmitter<Select>();
@Input() object: { id: number }; ✘
@Input() set object(object: { id: number }) { ✚ ➊
this.objectSelect = new FormControl(object); ✚
} ✚
@Input() url: string;
```
* ➊ 将object由输入的属性变更为输入函数。当每次有最新的obejct传入时,都会执行一次该方法中的内容。
去除测试信息后再测试:
![](https://img.kancloud.cn/4c/cf/4ccf3633116377437a7512d4dac63d2f_1115x331.gif)
已自动选中。
# 总结
同步输入与异步输入在此均发生组件使用`@Input()`接收数据的过程:
同步输入:先有变量,后有依赖于该变量的组件。
异步输入:在组件构造时,组件依赖的变量无值(undefined),组件构建完成后,变量被赋予了新值。
`ngOnInit`会在组件构造时且仅在组件构造时被自动执行一次,所以:
同步输入:执行`ngOnInit`时,输入的变量的值已存在,即是组件想要的真实的值。输入值发生变化后,`ngOnInit`不会再被自动调用。
异步输入:执行`ngOnInit`时,输入的变量的值尚为undefined,非组件想要的真实的值。输入值发生变化后,`ngOnInit`不会再被自动调用。
综上:
* ➊ 某个组件如果在接收某个输入变量后需要进行一些逻辑处理时,应该使用`@Input() set 接收方法(变量名) { 这里放逻辑代码; }`的方法,选择组件便适用于这种情况:当传入的object发生变化时,应该`变更被默认选中的option`,而此时`变更被默认选中的option`则是需要执行的逻辑处理;
* ➋ 某个组件接收变量后不需要执行逻辑处理,则可以简写为`@Input() 变量名`。
* ➌ `ngOnInit`会在且仅会在构建组件时自动被执行1次。在该方法中设计一些逻辑代码时,应该充分的考虑到输入变量可能被调用者异步传入的情况。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.7](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.7) | \- |
| 通过 setter 截听输入属性值的变化 | [https://www.angular.cn/guide/component-interaction#intercept-input-property-changes-with-a-setter](https://www.angular.cn/guide/component-interaction#intercept-input-property-changes-with-a-setter) | 5 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用