原文連接:RxJS: Avoiding switchMap-Related Bugs
原文做者:Nicholas Jamieson;發表於2018年3月12日
譯者:yk;如需轉載,請註明出處,謝謝合做!
翻譯內容有些部分借鑑了 vannxy 的 RxJS: 避免 switchMap 的相關 Bug,深表感謝。前端
不久前,Victor Savkin 發佈了一篇關於在 Angular 應用中使用 NgRx effects 時,因濫用 switchMap
而致使的一個不易察覺的 bug 的推文:git
Victor Savkin
@victorsavkingithub我看過的每一個 Angular 應用都由於 switchMap 的使用不當而產生不少錯誤。這是跟 RxJS 有關的 issues 的最大來源。 #angulartypescript
讓咱們以購物車爲例,看看下面的 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
。讓咱們來看看這些操做符,瞭解它們之間的差別,並決定哪一個操做符最適合購物車場景。
若是將 switchMap
替換爲 mergeMap
,effect/epic 將同時處理每一個已調來的操做。也就是說,被掛起的移除操做將不會被停止;後端請求會被同時發起,當移除完成後再處理響應。
須要重點注意的是,因爲操做是併發處理的,因此響應的順序可能與請求的順序不一樣。舉個例子,若是用戶依次點擊了兩個商品的「移出購物車」按鈕,後點擊的商品可能會先被移除。
在購物車場景裏,商品的移除順序並不重要,因此使用 mergeMap
來代替 switchMap
可解決這個問題。
雖然從購物車中移除商品的順序可能可有可無,但也有一些操做對執行順序是有嚴格要求的。
再舉個例子,若是咱們的購物車有一個用於增長商品數量的按鈕,則以正確的順序來處理調度的操做是很是重要的。不然,先後端的商品數量可能最終會不一樣步。
對於順序十分重要的操做,咱們應該使用 concatMap
,其實 concatMap
就至關於使用 mergeMap
時將其「容許併發量」參數 concurrent
設置爲 1
。也就是說,使用 concatMap
的 effect/epic 每次只會處理一個後端請求,每一個操做都會按照它們被調用的順序排隊。
concatMap
是安全且保守的選擇。若是你不肯定在 effect/epic 中使用何種 flattening operator 時,就用 concatMap
吧。
每當相同類型的操做被調用時,使用 switchMap
會停止以前已被掛起的後端請求。這使 switchMap
對於增長、修改以及刪除操做來講不那麼安全。甚至在處理讀操做時也會引入 bug 。
switchMap
是否適合特定的讀操做取決於當另外一個相同類型的操做被調用時,後端對先前操做作出的響應是否還有用。讓咱們來看看一個使用 switchMap
的操做是如何引入 bug 的。
若是咱們的購物車中的每一個商品都有一個「詳情」按鈕,用於在行內顯示一些商品的詳細信息,effect/epic 則使用 switchMap
來處理該按鈕的點擊動做,這裏又引入了一個競爭條件。若是用戶一連點擊了多個商品的「詳情」按鈕,那麼這些被點擊的商品是否會顯示詳細信息則一樣取決於用戶點擊按鈕的頻率。
和 RemoveFromCart
操做同樣,使用 mergeMap
就能夠解決這個問題了。
switchMap
只應當用於 effects/epics 中的讀操做處理,而且當另外一個相同類型的操做被調用時,後端對先前操做作出的響應可被丟棄。
讓咱們來看看 switchMap
的一個適用場景。
若是咱們的購物車要顯示商品的總價加上運費,對購物車內容作出的每一個更改都會觸發一次 GetCartTotal
操做。這時在 effect/epic 中使用 switchMap
來處理 GetCartTotal
是徹底合適的。
若是當 effect/epic 正在處理一個 GetCartTotal
操做時,購物車的內容發生了變更,那麼對當前處理中的請求作出的響應將會是過期的,也就是更改以前購物車內的商品總數,所以停止正在處理中的請求是沒有任何問題的。事實上,相較於掛起請求,等其完成以後再將其忽略,或更有甚者把過時的響應數據也渲染出來,直接停止請求會是更好的選擇。
exhaustMap
或許是最鮮爲人知的一個 flattening operator 了,但它很好解釋:你能夠認爲它是 switchMap
的對立物。
當使用 switchMap
時,以前掛起的後端請求會被停止,也就是說更傾向於處理最新調來的操做。而當使用 exhaustMap
時,若是當前有正在處理的後端請求,那麼新調來的操做都會被忽略。
讓咱們來看看 exhaustMap
的一個適用場景。
開發人員應該對有一類用戶再熟悉不過了:按鈕狂擊者。當他們點擊一個按鈕卻發現什麼都沒發生時,就會繼續點點點一直點。
假如咱們的購物車有一個刷新按鈕,effect/epic 使用 switchMap
來處理刷新,那麼每次點擊按鈕都會停止先前被掛起的刷新操做。因此狂點按鈕沒有任何意義,並且可能使用戶等待更長的時間,直到刷新被執行。
若是在 effect/epic 中使用 exhaustMap
來替代 switchMap
處理刷新的話,多餘的點擊將會被忽略。
總而言之,當你須要在一個 effect/epic 中使用 flattening operator 時,你應該:
concatMap
;(這也是一個保守的選擇,由於其總會按照預期運行)mergeMap
;switchMap
;exhaustMap
。我曾在 rxjs-tslint-rules
包裏添加了一條 rxjs-no-unsafe-switchmap
規則。
該規則能夠識別 NgRx effects 和 redux-observable
epics,並肯定它們的操做類型,而後根據操做類型搜索具體的動詞(例如:add
,update
,remove
等等)。它有一些合理的默認值,若是你以爲這些默認值過於常規,也能夠對它進行配置。
在啓用了該條規則後,我在我去年寫的一些應用上運行了 TSLint ,發現了很多 effects 都在以不安全的方式使用 switchMap
。因此,謝謝你的推文,Victor。