背景:當前輸入框的formControl設定了非同步驗證器,會根據當前的值進行請求後臺,判斷資料庫中是否存在。
原版非同步驗證器:
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()
)
};
}
- 新增非同步驗證器
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
看一下控制檯列印的結果:
圖中左邊的號碼代表這個value列印的次數
可以看到列印的次數是逐漸遞增的,這就說明了每次產生的訂閱事件,是沒有消失的。每次都新增一個訂閱事件, 以前的也跟著列印新的值。
在數量到一定多的時候會造成卡頓。
當我輸入這麼多的時候明顯感覺到卡頓。
當然在實際場景中可能不會有這麼多,這些訂閱事件也會在元件銷燬時跟著銷燬。
first()的使用
之前也不理解first的使用,看學長的文章之後才明白,first()來避免多次地這樣返回值。
所以我們產生的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()模擬時間的非同步流逝。
官方測試程式碼:
仿照測試程式碼:
我在tick()前後,列印了new Date(),也就是當時的時間,結果是什麼呢?
可以看到第一個列印了17:19:30,也就是當時測試的時間。
但是在tick(10000000)後,列印的時間是20:06:10, 達到了一個未來的時間。
並且,這兩條語句幾乎是同時列印的,也就是說,單元測試並沒有讓我們真的等待10000000ms。
所以經過測試時候我們就可以使用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();
}));
題外
寫後臺的時候還遇到了一個錯誤:
它說我color沒有設定預設值,
但是回去一看明明已經設定了。
打了很多斷點都沒發現問題。
後來到資料庫一看,好傢伙,怎麼有兩個,一個是colour,一個是color.
之後翻看之前提交的程式碼,發現是之前用的是color,後面換成了colour。
但是我jpa hibernate設定的是update,所以資料庫對應執行的是更新,所以上次的欄位並沒有刪除,這才導致了資料庫有兩個欄位。之後刪除其中一個了就沒事了。
jpa:
hibernate:
ddl-auto: update