angular异步验证器防抖

背景:当前输入框的formControl设置了异步验证器,会根据当前的值进行请求后台,判断数据库中是否存在。

image.png

原版异步验证器:

 vehicleBrandNameNotExist(): AsyncValidatorFn {
    return (control: AbstractControl): Observable => {
      if (control.value === '') {
        return of(null);
      }
      return this.vehicleBrandService.existByName(control.value).pipe(map(exists => exists ? {vehicleBrandNameExist: true} : null));
    };
  }

但是测试下来发现,该异步验证器触发的太频繁了。输入框每输入一个字母都会对后台进行请求,不利于节省资源。

防抖节流

这个相关的操作叫做防抖和节流。什么是防抖和节流?有什么区别?

本质上是一种优化高频率执行代码的一种手段。

比如浏览器的鼠标点击,键盘输入等事件触发时,会高频率地调用绑定在事件上的回调函数,一定程度上影响着资源的利用。

为了优化,我们需要 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率。

定义:

防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

举个例子来说明:

乘坐地铁,过闸机时,每个人进入后3秒后门关闭,等待下一个人进入。

闸机开之后,等待3秒,如果中又有人通过,3秒等待重新计时,直到3秒后没人通过后关闭,这是防抖

闸机开之后,每3秒后准时关闭一次,间隔时间执行,这是节流

代码实现:

防抖操作恰好符合我们的需求。

找异步验证器中防抖的代码实现中恰好看到了liyiheng学长的文章:
https://segmentfault.com/a/11...,于是便参考了一下。

这里仅是说明angular中formContorl异步验证器如何防抖的步骤:

1.创建(改写)异步验证器

vehicleBrandNameNotExist(): AsyncValidatorFn {
    return (control: AbstractControl): Observable => {
      if (control.value === '') {
        return of(null);
      }
      return control.valueChanges.pipe(
        // 防抖时间,单位毫秒
        debounceTime(1000),
        // 过滤掉重复的元素
        distinctUntilChanged(),
        // 调用服务, 获取结果
        switchMap(value => this.vehicleBrandService.existByName(value)),
        // 对结果进行处理,null表示正确,对象表示错误
        map((exists: boolean) => (exists ? {vehicleBrandNameExist: true} : null)),
        // 每次验证的结果是唯一的,截断流
        first()
      )
    };
  }
  1. 添加异步验证器
let formControl = new FormControl('', [], asyncValidate.vehicleBrandNameNotExist());

之后我们在v层在相关的标签上绑定该fromControl就可以了。

疑惑

相关操作到这里就结束了,能够正常使用了。

但是改写之后还有些疑惑。

原来的版本是这么使用的:

existByName结果返回一个Observable

return this.vehicleBrandService.existByName(...)

改写后是这么使用的:

return control.valueChanges.pipe(...

改写后使用了valueChanges,它使得每当控件的值在更改时,它都会发出一个事件。

那么,每次调用异步验证器之后,我们每次都用valueChanges,那么是不是每次都会多个返回?
产生了多个observabel?

于是我进行了测试:

每次异步验证的时候都订阅该observable,打印一下value的值。

control.valueChanges.subscribe((value) => {
        console.log(value);
})

我在输入框逐渐输入1 2 3 4 5 6 7 8 9
看一下控制台打印的结果:

angular异步验证器防抖_第1张图片

图中左边的号码代表这个value打印的次数

可以看到打印的次数是逐渐递增的,这就说明了每次产生的订阅事件,是没有消失的。每次都新增一个订阅事件, 以前的也跟着打印新的值。

解惑:

之后仔细看了学长的文章之后才明白:这里是用first()来避免多次地这样返回值。

angular异步验证器防抖_第2张图片

所以我们产生这么多的订阅事件没关系,让它返回第一个值就好

return control.valueChanges.pipe(
           first() 
)

实际上这些订阅事件还是存在的,我们只是让它返回了第一个值,剩下的这些observable我们没有处理。在数量到一定多的时候会造成卡顿。
当我输入这么多的时候明显感觉到卡顿。
angular异步验证器防抖_第3张图片

当然在实际场景中可能不会有这么多,这些订阅事件也会在组件销毁时跟着销毁。

单元测试

一个好的功能要有一个好的单元测试。

 1 it('should create an instance', async () => {
 2   expect(asyncValidate).toBeTruthy();
 3   let formControl = new FormControl('', [], asyncValidate.vehicleBrandNameNotExist());
 4   formControl.setValue('重复车辆品牌');
 5    // 等待防抖结束
 6   await new Promise(resolve => setTimeout(resolve, 1000));

 7   getTestScheduler().flush();
 8   expect(formControl.errors.vehicleBrandNameExist).toBeTrue();
     ...
}));

原来的时候我写的单元测试说这样的,

等待防抖结束我用了await new Promise 以及setTimeout。执行到第8行的时候,让线程等待1秒。

经过老师指正之后,发现这样并不好。假如某个测试需要等待一个小时,那么我们的执行时间就需要1个小时,这显然是不现实的。

所以这里用到了fakeAsync;

fakeAsync;

fakeAsync,字面上就是假异步,实际上还是同步进行的。

使用tick()模拟时间的异步流逝。

官方测试代码:
angular异步验证器防抖_第4张图片

仿照测试代码:

我在tick()前后,打印了new Date(),也就是当时的时间,结果是什么呢?

angular异步验证器防抖_第5张图片

可以看到第一个打印了17:19:30,也就是当时测试的时间。

但是在tick(10000000)后,打印的时间是20:06:10, 达到了一个未来的时间。

并且,这两条语句几乎是同时打印的,也就是说,单元测试并没有让我们真的等待10000000ms。

image.png

所以经过测试时候我们就可以使用tick(1000)和fakeAsync模拟防抖时间结束了。

 it('should create an instance', fakeAsync( () => {
    expect(asyncValidate).toBeTruthy();
    let formControl = new FormControl('', [], asyncValidate.vehicleBrandNameNotExist());
    formControl.setValue('重复车辆品牌');
    // 等待防抖结束
    tick(1000);
    getTestScheduler().flush();
    expect(formControl.errors.vehicleBrandNameExist).toBeTrue();

  }));

题外

写后台的时候还遇到了一个错误:
angular异步验证器防抖_第6张图片

它说我color没有设置默认值,
但是回去一看明明已经设置了。

image.png

打了很多断点都没发现问题。

后来到数据库一看,好家伙,怎么有两个,一个是colour,一个是color.
angular异步验证器防抖_第7张图片

之后翻看之前提交的代码,发现是之前用的是color,后面换成了colour。

但是我jpa hibernate设置的是update,所以数据库对应执行的是更新,所以上次的字段并没有删除,这才导致了数据库有两个字段。之后删除其中一个了就没事了。

jpa:
    hibernate:
      ddl-auto: update

你可能感兴趣的:(angular)