angular非同步驗證器防抖

weiewiyi發表於2022-03-29

背景:當前輸入框的formControl設定了非同步驗證器,會根據當前的值進行請求後臺,判斷資料庫中是否存在。

image.png

原版非同步驗證器:

 vehicleBrandNameNotExist(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      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<ValidationErrors | null> => {
      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就可以了。

疑惑

相關操作到這裡就結束了,能夠正常使用了。

但是改寫之後還有些疑惑。

原來的版本是這麼使用的:

return this.vehicleBrandService.existByName(...)

改寫後是這麼使用的:

return control.valueChanges.pipe(...

改寫後使用了valueChanges,也就是產生了一個observable,它使得每當控制元件的值在更改時,它都會發出一個事件。

那麼,每次呼叫非同步驗證器之後,我們每次都用valueChanges,每次都產生一個observable,那麼是不是之前的產生的observabel還在發出事件?沒有被清除?


於是我進行了測試:

每次非同步驗證的時候都訂閱該observable,列印一下value的值。

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

我在輸入框逐漸輸入1 2 3 4 5 6 7 8 9
看一下控制檯列印的結果:

image.png

圖中左邊的號碼代表這個value列印的次數

可以看到列印的次數是逐漸遞增的,這就說明了每次產生的訂閱事件,是沒有消失的。每次都新增一個訂閱事件, 以前的也跟著列印新的值。

image.png


在數量到一定多的時候會造成卡頓。

當我輸入這麼多的時候明顯感覺到卡頓。
image.png

當然在實際場景中可能不會有這麼多,這些訂閱事件也會在元件銷燬時跟著銷燬。


first()的使用

之前也不理解first的使用,看學長的文章之後才明白,first()來避免多次地這樣返回值。

image.png

所以我們產生的observable一直處於pending狀態,需要用first讓它返回第一個值就好。

return control.valueChanges.pipe(
           first() 
)

單元測試

一個好的功能要有一個好的單元測試。

 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()模擬時間的非同步流逝。

官方測試程式碼:
image.png

仿照測試程式碼:

我在tick()前後,列印了new Date(),也就是當時的時間,結果是什麼呢?

image.png

可以看到第一個列印了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();

  }));

題外

寫後臺的時候還遇到了一個錯誤:
image.png

它說我color沒有設定預設值,
但是回去一看明明已經設定了。

image.png

打了很多斷點都沒發現問題。

後來到資料庫一看,好傢伙,怎麼有兩個,一個是colour,一個是color.
image.png

之後翻看之前提交的程式碼,發現是之前用的是color,後面換成了colour。

但是我jpa hibernate設定的是update,所以資料庫對應執行的是更新,所以上次的欄位並沒有刪除,這才導致了資料庫有兩個欄位。之後刪除其中一個了就沒事了。

jpa:
    hibernate:
      ddl-auto: update

相關文章