首頁 > 軟體

angular非同步驗證器防抖範例詳解

2022-03-31 19:00:44

背景:

當前輸入框的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://www.jb51.net/article/175497.htm,於是便參考了一下。

這裡僅是說明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是不是同一個?

於是我進行了測試:
原理:多次呼叫非同步驗證器,並快取ovservable,如果不相同則輸出 “不相等”

測試結果:如圖,只是在第一次初始化的時候輸出了不相等,因為第一次observable為undefined, 在有值之後,多次呼叫非同步驗證器發現observabel始終是同一個

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

補充

後面谷歌之後發現了一種比較簡潔也好理解的方法:

不用呼叫first()之類的操作符, 把timer()的返回值作為一個observable就可以了。

time的作用在這裡:
https://rxjs-cn.github.io/lea...

簡單來說就是當 timer 結束時發出一個值。

這個原理猜測可能是當timer還沒有結束並重復呼叫非同步驗證器時,表單就不管這個timer了,轉而關注新的。

當然只是猜測,有機會再補充,經過測試防抖功能是正常的。

export class VehicleBrandAsyncValidator {
  /**
   * 防抖時間
   */
  debounceTime = 1000;
  
  constructor(private vehicleBrandService: VehicleBrandService) { }

  /**
   * 驗證方法,車輛品牌名稱
   */
  vehicleBrandNameNotExist(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (control.value === '') {
        return of(null);
      }
      return timer(this.debounceTime).pipe(
        // 呼叫服務, 獲取結果
        switchMap(() => this.vehicleBrandService.existByName(control.value)),
        // 對結果進行處理,null表示正確,物件表示錯誤
        map((exists: boolean) => (exists ? {vehicleBrandNameExist: true} : null)),
      )
    };
  }
}

總結

到此這篇關於angular非同步驗證器防抖的文章就介紹到這了,更多相關angular非同步驗證器防抖內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com