[譯] RxJS: 避免因濫用 switchMap 而致使錯誤

原文連接:RxJS: Avoiding switchMap-Related Bugs
原文做者:Nicholas Jamieson;發表於2018年3月12日
譯者:yk;如需轉載,請註明出處,謝謝合做!
翻譯內容有些部分借鑑了 vannxyRxJS: 避免 switchMap 的相關 Bug,深表感謝。前端

不久前,Victor Savkin 發佈了一篇關於在 Angular 應用中使用 NgRx effects 時,因濫用 switchMap 而致使的一個不易察覺的 bug 的推文:git

Victor Savkin
@victorsavkingithub

我看過的每一個 Angular 應用都由於 switchMap 的使用不當而產生不少錯誤。這是跟 RxJS 有關的 issues 的最大來源。 #angulartypescript

那麼這個 bug 是什麼呢?

讓咱們以購物車爲例,看看下面的 effect 和 epic 是怎麼濫用 switchMap 的,而後咱們再考慮用一些替代的操做符。redux

這是一個濫用 switchMap 的 NgRx effect:後端

@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)))
    )
  )
);
複製代碼

這是一個與之等價的 redux-observable epic:安全

const removeFromCart = actions$ => actions$.pipe(
  ofType(actions.REMOVE_FROM_CART),
  switchMap(action => backend
    .removeFromCart(action.payload)
    .pipe(
      map(response => actions.removeFromCartFulfilled(response)),
      catchError(error => of(actions.removeFromCartRejected(error)))
    )
  )
);
複製代碼

咱們的購物車列出了用戶打算購買的商品,每一個商品都有一個「移出購物車」按鈕。點擊該按鈕就會將 RemoveFromCart 動做調至 effect/epic ,後者與後端通訊並移除該商品。併發

大多數狀況下,這都將如期運行。然而,使用 switchMap 會在這裏引入競爭條件(race condition)。this

若是用戶一連點擊了多個商品的「移出購物車」按鈕,則結果將取決於按鈕點擊的頻率。spa

若是用戶在 effect/epic 與後端通訊時再次點擊了按鈕,以前的移除操做就會被掛起,而在這使用 switchMap 則會停止以前被掛起的操做。

所以,根據按鈕點擊的頻率,程序可能會:

  • 刪除全部被點擊的商品;
  • 僅刪除部分被點擊的商品;
  • 在後端刪除了部分被點擊的商品,而前端的購物車卻沒反應。

很明顯,這是一個 bug。

不幸的是,當須要使用 flattening operator(打平操做符)時,switchMap 一般會被建議是首選,但這並非在全部場景下都是安全的。

RxJS 共有四個 flattening operator 可供選擇:

  • mergeMap(也稱爲 flatMap );
  • concatMap
  • switchMap
  • exhaustMap

讓咱們來看看這些操做符,瞭解它們之間的差別,並決定哪一個操做符最適合購物車場景。

mergeMap/flatMap

若是將 switchMap 替換爲 mergeMap,effect/epic 將同時處理每一個已調來的操做。也就是說,被掛起的移除操做將不會被停止;後端請求會被同時發起,當移除完成後再處理響應。

須要重點注意的是,因爲操做是併發處理的,因此響應的順序可能與請求的順序不一樣。舉個例子,若是用戶依次點擊了兩個商品的「移出購物車」按鈕,後點擊的商品可能會先被移除。

在購物車場景裏,商品的移除順序並不重要,因此使用 mergeMap 來代替 switchMap 可解決這個問題。

concatMap

雖然從購物車中移除商品的順序可能可有可無,但也有一些操做對執行順序是有嚴格要求的。

再舉個例子,若是咱們的購物車有一個用於增長商品數量的按鈕,則以正確的順序來處理調度的操做是很是重要的。不然,先後端的商品數量可能最終會不一樣步。

對於順序十分重要的操做,咱們應該使用 concatMap,其實 concatMap 就至關於使用 mergeMap 時將其「容許併發量」參數 concurrent 設置爲 1 。也就是說,使用 concatMap 的 effect/epic 每次只會處理一個後端請求,每一個操做都會按照它們被調用的順序排隊。

concatMap 是安全且保守的選擇。若是你不肯定在 effect/epic 中使用何種 flattening operator 時,就用 concatMap 吧。

switchMap

每當相同類型的操做被調用時,使用 switchMap 會停止以前已被掛起的後端請求。這使 switchMap 對於增長、修改以及刪除操做來講不那麼安全。甚至在處理讀操做時也會引入 bug 。

switchMap 是否適合特定的讀操做取決於當另外一個相同類型的操做被調用時,後端對先前操做作出的響應是否還有用。讓咱們來看看一個使用 switchMap 的操做是如何引入 bug 的。

若是咱們的購物車中的每一個商品都有一個「詳情」按鈕,用於在行內顯示一些商品的詳細信息,effect/epic 則使用 switchMap 來處理該按鈕的點擊動做,這裏又引入了一個競爭條件。若是用戶一連點擊了多個商品的「詳情」按鈕,那麼這些被點擊的商品是否會顯示詳細信息則一樣取決於用戶點擊按鈕的頻率。

RemoveFromCart 操做同樣,使用 mergeMap 就能夠解決這個問題了。

switchMap 只應當用於 effects/epics 中的讀操做處理,而且當另外一個相同類型的操做被調用時,後端對先前操做作出的響應可被丟棄。

讓咱們來看看 switchMap 的一個適用場景。

若是咱們的購物車要顯示商品的總價加上運費,對購物車內容作出的每一個更改都會觸發一次 GetCartTotal 操做。這時在 effect/epic 中使用 switchMap 來處理 GetCartTotal 是徹底合適的。

若是當 effect/epic 正在處理一個 GetCartTotal 操做時,購物車的內容發生了變更,那麼對當前處理中的請求作出的響應將會是過期的,也就是更改以前購物車內的商品總數,所以停止正在處理中的請求是沒有任何問題的。事實上,相較於掛起請求,等其完成以後再將其忽略,或更有甚者把過時的響應數據也渲染出來,直接停止請求會是更好的選擇。

exhaustMap

exhaustMap 或許是最鮮爲人知的一個 flattening operator 了,但它很好解釋:你能夠認爲它是 switchMap 的對立物。

當使用 switchMap 時,以前掛起的後端請求會被停止,也就是說更傾向於處理最新調來的操做。而當使用 exhaustMap 時,若是當前有正在處理的後端請求,那麼新調來的操做都會被忽略。

讓咱們來看看 exhaustMap 的一個適用場景。

開發人員應該對有一類用戶再熟悉不過了:按鈕狂擊者。當他們點擊一個按鈕卻發現什麼都沒發生時,就會繼續點點點一直點。

假如咱們的購物車有一個刷新按鈕,effect/epic 使用 switchMap 來處理刷新,那麼每次點擊按鈕都會停止先前被掛起的刷新操做。因此狂點按鈕沒有任何意義,並且可能使用戶等待更長的時間,直到刷新被執行。

若是在 effect/epic 中使用 exhaustMap 來替代 switchMap 處理刷新的話,多餘的點擊將會被忽略。

總結

總而言之,當你須要在一個 effect/epic 中使用 flattening operator 時,你應該:

  • 當操做不能被停止/忽略,且必須保證執行順序的狀況下,使用 concatMap;(這也是一個保守的選擇,由於其總會按照預期運行)
  • 當操做不能被停止/忽略,但執行順序並不重要的狀況下,使用 mergeMap
  • 當處理讀操做時,且當另外一個相同類型的操做被調用時,先前的請求應當被停止的狀況下,使用 switchMap
  • 當相同類型的操做應當被忽略的狀況下,使用 exhaustMap

使用 TSLint 來避免濫用 switchMap

我曾在 rxjs-tslint-rules 包裏添加了一條 rxjs-no-unsafe-switchmap 規則。

該規則能夠識別 NgRx effects 和 redux-observable epics,並肯定它們的操做類型,而後根據操做類型搜索具體的動詞(例如:addupdateremove 等等)。它有一些合理的默認值,若是你以爲這些默認值過於常規,也能夠對它進行配置

在啓用了該條規則後,我在我去年寫的一些應用上運行了 TSLint ,發現了很多 effects 都在以不安全的方式使用 switchMap。因此,謝謝你的推文,Victor。

相關文章
相關標籤/搜索