上一小节的测试中,发现在进行学生编辑时班级选择组件无法自动选中学生所在的班级。下面来尝试查找并解决该问题。和上节的测试准备工作一致,经过一系列的启动与数据准备。来到学生编辑功能的测试环节: ![](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 |