動態

詳情 返回 返回

Rxjs SwitchMap 的一些容易犯的錯誤和替代方案 - 動態 詳情

下面是一個在 Effect 裏使用 SwitchMap 的例子:從購物車裏移除某個行項目

@Effect()
public removeFromCart = this.actions.pipe(
  ofType(CartActionTypes.RemoveFromCart),
  switchMap(action => this.backend
    .removeFromCart(action.payload)
    .pipe(
      map(response => new RemoveFromCartFulfilled(response)),
      catchError(error => of(new RemoveFromCartRejected(error)))
    )
  )
);

購物車列出了用户打算購買的商品,每個商品都有一個從購物車中刪除商品的按鈕。 單擊該按鈕會將 RemoveFromCart 操作分派給與應用程序後端通信的對應 API,並查看從購物車中刪除的項目。

這段代碼看似能夠正常運行,但實際上 switchMap 的使用,引入了競態條件(race condition)。

如果用户單擊購物車中多個項目的刪除按鈕,會出現什麼樣的行為?

根據客户點擊按鈕的速度不同,應用程序可能會:

  1. 從購物車中刪除所有點擊的物品,比如客户點擊一個行項目的刪除按鈕,等刪除操作在後台成功執行之後,再點擊第二個行項目。
  2. 客户飛快地點擊了前兩個行項目的刪除按鈕。第一個行項目的刪除請求正在發送往後台服務器的過程當中,則第二個按鈕的點擊,會取消第一個行項目的刪除請求。最後僅僅第二個行項目被刪除了。
  3. 客户依次點擊了前兩個行項目的刪除按鈕。第一個刪除請求已經抵達後台,正在執行後台的刪除操作。第二個請求也到達了後台。此時的行為,取決於後台 API 從 cart 上刪除行項目時,是否給當前的 cart 加了鎖。

我們考慮一下是否能用如下的 Operator 來替代 SwitchMap.

mergeMap/flatMap

如果 switchMap 被 mergeMap 替換,則 effect 的代碼將同時處理每個調度的動作。

也就是説,pending 的刪除不會被中止;後端請求將同時發生。請求完成時,Effect 會 dispatch 對應的 action.

需要注意的是,由於操作的併發處理,響應的順序可能與請求的順序不匹配。 例如,如果用户單擊第一個和第二個項目的刪除按鈕,則第二個項目的刪除可能發生在第一個項目的刪除之前。

對於購物車裏刪除行項目的場景而言,刪除的順序並不重要,因此使用 mergeMap 而不是 switchMap 可以修復該錯誤,規避潛在的竟態條件。

concatMap

從購物車中移除商品的順序可能無關緊要,但通常有一些操作對排序很重要。

例如,如果我們的購物車有一個增加商品數量的按鈕,那麼以正確的順序處理分派的操作很重要。 否則,前端購物車中的數量最終可能與後端購物車中的數量不同步。

對於排序很重要的操作,應使用 concatMap.

concatMap 相當於使用併發為 1 的 mergeMap. 也就是説,使用 concatMap 的 effect 代碼一次將只處理一個後端請求,並且操作按照它們被調度的順序排隊。

concatMap 是一個安全而保守的選擇。 當不確定在 Effect 中使用 SwitchMap,MergeMap 或者 concatMap 時,使用 concatMap 比較安全。

switchMap

每當調度相同類型的操作時,使用 switchMap 將看到掛起的後端請求中止。這使得 switchMap 對於創建、更新和刪除操作不安全。但是,它也可能為讀取操作引入錯誤。
switchMap 是否適用於特定的讀取操作取決於在分派另一個相同類型的操作後是否仍需要後端響應。讓我們看一下使用 switchMap 會引入錯誤的操作。
如果我們購物車中的每個商品都有一個詳細信息按鈕——用於顯示一些內聯詳細信息——並且處理詳細信息操作的效果/史詩使用 switchMap,則引入了競爭條件。如果用户點擊了幾個項目的詳細信息按鈕,是否顯示這些項目的詳細信息取決於用户點擊按鈕的速度。
與 RemoveFromCart 操作一樣,使用 mergeMap 可以修復錯誤。
switchMap 應該只在效果/史詩中用於讀取操作,並且僅在分派另一個相同類型的操作後不需要後端響應時使用。

讓我們看一下一個實用的 switchMap 使用場景。

如果我們的應用程序的購物車顯示商品的總成本加上運費,那麼對購物車內容的每次更改之後,都會觸發一個 GetCartTotal 的讀操作。

將 switchMap 用於處理 GetCartTotal 操作的做法是完全合適的。

如果 Effect 正在處理 GetCartTotal 操作時更改了購物車,則對 pending 請求的響應已經是陳舊的 - 它是更改之前購物車中項目的總數 - 因此中止掛起的讀操作請求是合理的。

事實上,中止這個不必要的讀請求比允許該讀請求完成然後忽略——或者更糟糕的是,在界面上顯示陳舊的響應更可取。

exhaustMap

exhaustMap 可能是最不為人所知的 flatterning 運算符,但它很容易解釋:它可以被認為是 switchMap 的反面。

如果使用 switchMap,pending 的後端請求將被中止,以支持最近一次分發的操作。

反之,如果使用了 exhaustMap,當有一個掛起的後端請求時,分派的動作將被忽略。

開發人員應該熟悉一種特殊類型的用户:傾向於不斷重複點擊同一個按鈕。特別是當不斷地點擊一個按鈕並且沒有任何響應時,這些用户會再次點擊它。

如果購物車有一個刷新按鈕,並且處理刷新的 Effect 代碼中使用 switchMap,則每次不斷的按鈕單擊都會中止之前觸發的刷新操作。

如果處理購物車刷新的 Effect 改為 ExhaustMap,則待處理的刷新請求將反過來忽略不斷重複的點擊。

總結

  • 將 concatMap 與既不應中止也不應忽略,必須保留其順序的操作一起使用。使用 concatMap 是一種保守的選擇,將始終以可預測的方式運行;
  • 將 mergeMap 與既不應該中止,也不應該忽略,並且先後順序不重要的動作一起使用;
  • 將 switchMap 與讀取操作一起使用,當分派另一個相同類型的操作時,之前的操作應該被中止,這種情況是 switchMap 的最佳適用場合。
  • 如果存在相同類型的操作處於待處理狀態時,新觸發的相同類型的操作應該被忽略,此時應該是一 exhaustMap.
user avatar vleedesigntheory 頭像 tangxinhebaodan 頭像 syntaxerror 頭像 beyondyinjl 頭像 weiliuhong 頭像 shenchendexiaodao 頭像 piano 頭像 htvlz 頭像
點贊 8 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.