7.4 RXJS : Hay dùng - quan1997ap/angular-app-note GitHub Wiki

1. Toán tử tap

Dù phân loại này có thể chủ quan, nhưng đây có lẽ là toán tử được sử dụng nhiều nhất đối với mọi lập trình viên RxJS.

Hãy xem tài liệu chính thức nói gì về nó:

Được sử dụng để thực hiện các hiệu ứng phụ cho các thông báo từ observable nguồn.

Nói cách khác, chúng ta có thể thực hiện một số hoạt động ngay khi Observable phát ra một sự kiện next, error hoặc complete, và chúng ta sẽ làm điều đó mà không làm thay đổi kết quả của Observable mà nó được kết nối.

Hãy xem ví dụ:

this.getDataFromServer()  
  .pipe(  
    tap((response) => this.originalResponse = response),  
    map(response => this.doSomethingWithResponse(response)),  
    tap(responseAfterMap => console.debug("transformed response", responseAfterMap))  
  )  
  .subscribe();

Ở đây, chúng ta đang sử dụng tap để lưu trữ phản hồi gốc trước (bởi vì chúng ta cần nó vì một lý do nào đó trong component của mình), và để xem cách nó đã được biến đổi bởi map sau này. Cái này làm rõ vì sao toán tử tap được sử dụng nhiều cho mục đích debugging; dòng code này, thật ra, sẽ có thể bị loại bỏ bởi developer ngay khi bạn hoàn thành việc debug.

2. Toán tử map

Mục đích của toán tử này là để biến đổi kết quả phát ra bởi Observable mà nó được gắn vào. Nó rất giống với hàm map của Array.

Một lần nữa, hãy xem ví dụ:

getVehicleInfo(vehiclePlate: string): Observable<CarInfo\> {  
  return this.http.get<CarInfo\>(\`https://someexampledomain.com/details/${vehiclePlate}\`)  
  .pipe(  
    map(vehicleData => {  
      return { model: vehicleData.model, color: vehicleData.color, isCar: vehicleData.type === 'CAR' };   
    })  
  );  
}

Dịch vụ web tuyệt vời của chúng ta trả về rất nhiều thuộc tính và metadata về chiếc xe có biển số vehiclePlate; tuy nhiên, frontend của chúng ta không cần những thông tin chi tiết đến như vậy vì nó chỉ hiển thị mô hình, màu sắc và xem liệu chiếc xe có phải là ô tô hay không.

3. Toán tử of

Chúng ta sử dụng toán tử này để tạo ra một Observable mà ngay lập tức phát ra giá trị (hoặc list các giá trị) mà chúng ta đã truyền vào.

Nó có thể được sử dụng cho nhiều mục đích; có lẽ mục đích phổ biến nhất là để mô phỏng một phản hồi dịch vụ web. Hãy để mình cho bạn xem:

getVehicleInfo(vehiclePlate: string): Observable<CarInfo\> {  
  return of({ model: "Ford Puma" , color: "Desert Island Blue", isCar: true });  
}

4. Toán tử switchMap

Hãy nói về một trong những toán tử yêu thích của mình! Mục đích của nó là để "chuyển đổi" một Observable với một Observable khác.

Một trường hợp sử dụng tiêu biểu là thanh tìm kiếm, cần thực hiện một lời gọi API mỗi khi người dùng nhập vào đó.

Hãy xem một chút code:

export class CarComponent {  
  form = new FormGroup({  
    search: new FormControls('')  
  });  
  carsList:CarInfo[] = [];  
  
  constructor(private searchService:SearchService) {  
    this.form.controls['search'].valueChanges.pipe(  
      switchMap(term => this.searchService.search(term))  
     )  
     .subscribe(list => this.carsList = list);  
  }  
}

Có thêm 2 toán tử khác cũng làm công việc tương tự: concatMap và mergeMap; tuy nhiên, switchMap khác với chúng do hiệu ứng hủy của nó, tránh race conditions.

Trong trường hợp của chúng ta, điều này có nghĩa là nếu người dùng tiếp tục nhập vào thanh tìm kiếm (sẽ thực hiện thêm các lời gọi API), chỉ kết quả cuối cùng sẽ được hiển thị.

5. Toán tử catchError

Toán tử này rất quan trọng bất cứ khi nào chúng ta muốn xử lý các lỗi Observable.

Thông thường, mỗi khi một lỗi xảy ra, kết nối giữa Observable và Subscriber sẽ bị đóng. Chúng ta có thể tránh đóng kết nối bằng cách sử dụng catchError! Cách nó hoạt động thực sự rất đơn giản: nó nhận đầu vào là một lỗi và nó trả về một Observable khác.

Hãy chỉnh sửa ví dụ trước để hiểu rõ hơn:

export class CarComponent {  
  form = new FormGroup({  
    search: new FormControls('')  
  });  
  carsList:CarInfo[] = [];  
  
  constructor(private searchService:SearchService) {  
    this.form.controls['search'].valueChanges.pipe(  
      switchMap(term => this.searchService.search(term)),  
      catchError(err => {    
        this.showErrorMessage(err);  
        return of([]);  
      })  
     )  
     .subscribe(list => this.carsList = list);  
  }  
}

Từ giờ trở đi, nếu một lỗi xảy ra (tức là một lỗi API), nó sẽ được hiển thị cho người dùng và Subscription sẽ không bị đóng; điều này có nghĩa là thanh tìm kiếm sẽ tiếp tục hoạt động như thể không có gì xảy ra, thực hiện một lời gọi API mỗi khi người dùng nhập vào đó.

6. Toán tử startWith

Toán tử này có thể được đánh giá cao đặc biệt với Hot Observables.

Nó phát ra một giá trị nhất định ngay sau khi Subscription.

Hãy chỉnh sửa (lại) ví dụ trước để xem nó làm gì:

export class CarComponent {  
  form = new FormGroup({  
    search: new FormControls('')  
  });  
  carsList:CarInfo[] = [];  
  
  constructor(private searchService:SearchService) {  
    this.form.controls['search'].valueChanges.pipe(  
      startWith(''),  
      switchMap(term => this.searchService.search(term)),  
      catchError(err => {    
        this.showErrorMessage(err);  
        return of([]);  
      })  
     )  
     .subscribe(list => this.carsList = list);  
  }   
}

Tại sao một dòng code nhỏ như vậy lại quan trọng?

Nếu người đọc đã chú ý, họ sẽ nhận thấy rằng có một vấn đề nhỏ với thanh tìm kiếm của chúng ta: ngay cả khi component đã được khởi tạo, người dùng sẽ không thấy bất kỳ chiếc xe nào trong danh sách cho đến khi họ nhập vào thanh tìm kiếm.

Sử dụng dòng code này, chúng ta vượt qua vấn đề này vì ngay khi component được init, nó sẽ thực hiện một lời gọi dịch vụ web cho phép chúng ta có được danh sách không lọc của các xe ngay từ đầu.

7. Toán tử debounceTime

VD: debounceTime(2000) user click button liên tục. Thì tính từ thời điểm click cuối + 2000ms thì giá trị sẽ được emit ra.

Đây là một trong những toán tử yêu thích khác của tôi. Hãy đọc xem tài liệu chính thức nói gì về nó:

Loại bỏ các giá trị phát ra mà thời gian phát ra tính từ giá trị trước đó nhỏ hơn giá trị thời gian đã thiết lập

Nó có nghĩa là gì? Hãy xem xét ví dụ về thanh tìm kiếm một lần nữa.

Có một vấn đề khác là: mỗi khi người dùng nhập vào đầu vào, ngay cả chỉ một ký tự, API sẽ được gọi. Như bạn có thể tưởng tượng, máy chủ không hài lòng về điều đó. Hãy tưởng tượng mọi người đều làm như vậy cùng một lúc... (nếu không phải là một cuộc tấn công DDoS thì chúng khá giống rồi DDos rồi đấy 😄).

Sử dụng debouceTime(n), giá trị của trường tìm kiếm sẽ chỉ được phát ra sau n mili giây; hơn nữa, nếu trong khi đó người dùng đã nhập thêm một ký tự, thêm n mili giây sẽ trôi qua trước khi phát ra giá trị.

Điều này cho phép chúng ta giảm số lượng các lời gọi API đến máy chủ của chúng ta một cách đáng kể.

Hãy chỉnh sửa ví dụ cuối cùng của chúng ta một lần nữa để thấy sự khác biệt:

export class CarComponent {  
  form = new FormGroup({  
    search: new FormControls('')  
  });  
  carsList:CarInfo[] = [];  
  
  constructor(private searchService:SearchService) {  
    this.form.controls['search'].valueChanges.pipe(  
      startWith(''),  
      debounceTime(300),  
      switchMap(term => this.searchService.search(term)),  
      catchError(err => {    
        this.showErrorMessage(err);  
        return of([]);  
      })  
     )  
     .subscribe(list => this.carsList = list);  
  }    
}

8. Toán tử delay

Đây là một trong những toán tử đơn giản nhất: như tên gọi, nó chỉ trì hoãn việc phát ra một giá trị.

Một trường hợp sử dụng điển hình là, cùng với toán tử of, để làm cho việc gọi API mô phỏng trở nên "thực tế" hơn.

getVehicleInfo(vehiclePlate: string): Observable<CarInfo> {  
  return of({ model: "Ford Puma" , color: "Desert Island Blue", isCar: true })  
    .pipe(delay(500));  
}

9. Toán tử distinctUntilChanged

Toán tử này chỉ phát ra một giá trị nếu nó khác với giá trị đã phát ra cuối cùng.

Đừng ghét mình vì điều này nhé, nhưng mình cần chỉnh sửa lại thanh tìm kiếm tuyệt vời của chúng ta một lần nữa để cho bạn thấy toán tử này có thể giúp ích cho chúng ta như thế nào. (Lần cuối mình hứa 😄)

export class CarComponent {  
  form = new FormGroup({  
    search: new FormControls('')  
  });  
  carsList:CarInfo[] = [];  
  
  constructor(private searchService:SearchService) {  
    this.form.controls['search'].valueChanges.pipe(  
      startWith(''),  
      distinctUntilChanged(),  
      debounceTime(300),  
      switchMap(term => this.searchService.search(term)),  
      catchError(err => {    
        this.showErrorMessage(err);  
        return of([]);  
      })  
     )  
     .subscribe(list => this.carsList = list);  
  }  
}

Và bây giờ... người dùng đã gõ điều gì đó vào thanh tìm kiếm; vì 300ms đã trôi qua nên component làm mới danh sách xe. Sau đó, người dùng vô tình gõ thêm một chữ cái, vì vậy anh ấy nhấn backspace để xóa nó. Thêm 300ms nữa trôi qua; tuy nhiên, API chưa được gọi vì distinctUntilChanged nhận ra rằng giá trị là giống với lần phát ra cuối cùng. Một lần nữa chúng ta đã tiết kiệm cho máy chủ của chúng ta khỏi một lần gọi API không cần thiết! 😃

Bây giờ câu hỏi đặt ra là: Làm sao mà toán tử này nhận biết được không có sự thay đổi nào xảy ra?

À, theo mặc định distinctUntilChanged sử dụng toán tử so sánh === vậy nên nó ok cho các chuỗi.

Nhưng mà, cũng có thể tạo ra một hàm so sánh tùy chỉnh và truyền nó như một tham số đầu vào cho toán tử:

distinctUntilChanged((prev, curr) => prev.toLowerCase() === curr.toLowerCase())

10. Toán tử filter

Toán tử cuối cùng (nhưng không kém phần quan trọng) chúng ta sẽ xem ở đây là toán tử filter. Giống như trường hợp của toán tử map, toán tử này cơ bản làm cùng một việc mà hàm filter của Array thực hiện cho các Array. Nói cách khác:

Nó phát ra các giá trị thoả mãn điều kiện đã cung cấp

Cùng xem cách nó hoạt động qua một ví dụ:

export class CarComponent {  
  vehicles = [  
    { model: "Ford Puma" , color: "Desert Island Blue", isCar: true },  
    { model: "Iveco S-WAY" , color: "Polar White", isCar: false},  
    { model: "Fiat 500" , color: "Gelato White", isCar: true }  
  ]  
    
  // returns only the vehicles which are cars.  
  getCarsList(): Observable<CarListItem[]> {  
    from(vehicles).pipe(  
      filter(vehicle => vehicle.isCar)  
    );  
  }  

10. Toán tử ConcatMap

https://viblo.asia/p/rxjs-su-dung-concatmap-va-mergemap-ByEZkgmYZQ0

- concatMap thường được sử dụng khi chúng ta muốn xử lý dữ liệu theo thứ tự. - Chỉ thực hiện gửi request tiếp theo khi request trước đó đã hoàn thành. Điều này sẽ đảm bảo dữ liệu được lưu trong database luôn là dữ liệu mới nhất.

Giả sử bạn cần làm tính năng tự động lưu nội dung form (textarea để nhập nội dung). Sử dụng concatMap có thể làm như sau:

const postId = 1;

this.form.valueChanges.pipe(
    concatMap(formData => {
        return this.http.put('/post/${postId}', formData)
    })
    .subscribe(
        result => console.log('Saved!')
    )
)

Ở ví dụ trên mỗi khi nội dung textarea thay đổi, chúng ta sẽ thực hiện gửi 1 http request (PUT request) lên server.

11. MergeMap

mergeMap thường được sử dụng khi chúng ta muốn xử lý các data đồng thời. Như ở ví dụ trên, nếu chúng ta thay concatMap bởi mergeMap, request tiếp theo có thể sẽ được gửi trước khi request trước đó hoàn thành, và như vậy có thể dẫn đến trường hợp dữ liệu được lưu trên database không phải là dữ liệu mới nhất của form.

Hãy thử một ví dụ với mergeMap. Lần này bạn cần build vote app. Mỗi lần click vào button Vote sẽ gửi 1 POST request lên server và +1 cho vote. Vì mỗi request sẽ tăng 1 vote. Request sau không cần đợi request trước đó hoàn thành nên có thể gửi các request đồng thời.

const btn = document.querySelector('#btn')

fromEvent(btn, 'click')
    .pipe(
        mergeMap(() => return this.http.post('/vote'))
    )
    .subscribe(
        result => console.log(result)
    )

Khi chỉ có 1 stream, map có lẽ là operator duy nhất bạn cần. Tuy nhiên khi có nhiều stream, sử dụng concatMap và mergeMap sẽ giúp việc xử lý data trở nên đơn giản hơn.

⚠️ **GitHub.com Fallback** ⚠️