歷來如此,便對麼?-----《狂人日記》html
在 react/redux 體系下的項目,在處理反作用(前端主要表如今異步問題)時,老是無腦上 redux-sage,前人鋪的路老是香的,可是前人爲何走這條路呢?今天咱們來挖挖他的前世此生。前端
咱們設想一下,若是不用 redux-sage,該如何處理異步問題哩?react
在開始以前,先講講爲何出現 redux 。git
話說隨着 web2.0 時代的到來,前端能夠作愈來愈多的事情,爲了解決日益複雜的前端單頁應用,JS 須要管理愈來愈多的狀態,多是服務端數據,多是UI狀態,甚至是動效或者臨時數據。而這些不斷在動態變化的狀態管理變得愈加地痛苦,開發者很難知道這些狀態何時,因爲什麼緣由,如何地變化。github
redux 認爲上述的複雜度來自於變化和異步。當這兩點混合起來時,應用的維護與管理會變得沒法預測,因此,須要一些方法來解決問題。web
而在計算機領域,複雜的東西每每能夠經過狀態機
這一律念獲得明確與細化,我的以爲 redux 的設計哲學是想將應用拆解爲可描述狀態
以及如何跳轉
的狀態機。編程
爲了讓應用的狀態變化可預測,redux 作了一下三個原則約定:json
其中,action 本質是一個 JS 的普通對象,reducers 約定爲純函數。redux
ok,前戲結束,咱們回到正題。api
在同步操做的場景下,redux 接收到 actions 對象後,dispatch => reducer 同步算出 state,總體的流程運轉以下:
這時,代碼大概長這樣:
1// action types
2export const ACTION_A = 'ACTION_A';
3export const ACTION_B = 'ACTION_B';
4
5// action creator
6export const actionA = (a) => ({
7 type: ACTION_A,
8 a
9});
10
11export const actionB = (b) => ({
12 type: ACTION_B,
13 b
14});
15
16// reducer
17export default function todos(state = initialState, action) {
18 switch (action.type) {
19 case ACTION_A:
20 return state.a
21 case ACTION_B:
22 return state.b
23
24 default:
25 return state
26 }
27}複製代碼
可是異步場景怎麼處理呢?咱們如何在一段異步任務結束後,根據他的結果分發不一樣的 action 呢?
可能會寫成這樣?
1// action creator
2export const actionFetch = (id) => ({
3 type: ACTION_FETCH,
4 id
5});
6
7// reducer
8export function fetch(state = initialState, action) {
9 switch (action.type) {
10 case ACTION_FETCH:
11 fetch(`http://www.subreddit.com/r/${action.id}.json`)
12 .then(
13 response => response.json(),
14 )
15 .then(json =>
16 // 能夠屢次 dispatch!
17 // 這裏,使用 API 請求結果來更新應用的 state。
18
19 ...
20
21 return resState;
22 )
23
24 default:
25 return state
26 }
27}複製代碼
等等,好像哪裏不對,reducer 中處理反作用好像違背了純函數的約定?
在下趙日天,今天就問問違背了會怎麼樣呢?
看看 redux 的官方解釋:
突變是一種不鼓勵的作法,由於它一般會打亂調試的過程,以及 React Redux 的
connect
函數:
- 對於調試過程, Redux DevTools 指望重放 action 記錄時可以輸出 state 值,而不會改變任何其餘的狀態。突變或者異步行爲會產生一些反作用,可能使調試過程當中的行爲被替換,致使破壞了應用。
- 對於 React Redux
connect
來講,爲了肯定一個組件(component)是否須要更新,它會檢查從mapStateToProps
中返回的值是否發生改變。爲了提高性能,connect
使用了一些依賴於不可變 state 的方法。而且使用淺引用(shallow reference)來檢測狀態的改變。這意味着直接修改對象或者數組是不會被檢測到的而且組件不會被從新渲染。Redux 深受函數式編程的影響,創造性的不支持反作用的執行。尤爲是 reducer,
必須是符合(state, action) => newState
的純函數。然而,Redux 的 middleware 能攔截分發的 action 並添加額外的複雜行爲,還能夠添加反作用。
emmm,好像有那麼點道理,那麼,不在 reducer 中處理異步邏輯,只能在 action 中搞了?可是 action 不是隻能是簡單對象嗎?
準確地來講, reducer 只能處理簡單對象的 action,因此,只能在 dispatch(action) 的過程當中作文章了。
這裏就扯出來了 redux 的中間件機制 middleware
。
每一個 middleware 接受
Store
的dispatch
和getState
函數做爲命名參數,並返回一個函數。該函數會被傳入 被稱爲next
的下一個 middleware 的 dispatch 方法,並返回一個接收 action 的新函數,這個函數能夠直接調用next(action)
,或者在其餘須要的時刻調用,甚至根本不去調用它。調用鏈中最後一個 middleware 會接受真實的 store 的dispatch
方法做爲next
參數,並藉此結束調用鏈。因此,middleware 的函數簽名是({ getState, dispatch }) => next => action
。
Middleware 最多見的使用場景是無需引用大量代碼或依賴相似 Rx 的第三方庫實現異步 actions。這種方式可讓你像 dispatch 通常的 actions 那樣 dispatch 異步 actions。
流程大概變成了這樣:
React+Redux Cycle(來源:https://www.youtube.com/watch?v=1QI-UE3-0PU)
利用 redux 提供的中間件機制能夠寫成以下形式:
1//action creator
2export function fetchPosts(subreddit) {
3 return function (dispatch) {
4 dispatch(requestPosts(subreddit))
5
6 return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
7 .then(
8 response => response.json(),
9 )
10 .then(json =>
11 dispatch(FETCH_SUCCESS))
12 )
13 }
14}
15
16
17//reducer
18const reducer = function(oldState, action) {
19 switch(action.type) {
20 case FETCH_SUCCESS :
21 return successState;
22 case FETCH_FAILED :
23 return errorState;
24 }
25}複製代碼
但是 dispatch 只能普通對象,還要繼續改造 dispatch 嗎?
固然能夠,不過 redux-thunk 已經幫你作好了這件事。
redux-thunk 的思路是:將 action 由簡單對象變爲函數,在函數中處理反作用,利用 redux 的 applyMiddleware() 在dispatch 機制中接受 函數形態的 action,在 middleware 鏈中最後一個任務處理完後發佈同步的 action 進入常規的同步過程。
1//action creator
2export function fetchPosts(subreddit) {
3 // 控制反轉!
4 // 返回一個接收 `dispatch` 的函數。
5 // Thunk middleware 知道如何把異步的 thunk action 轉爲普通 action。
6 return function (dispatch) {
7 dispatch(requestPosts(subreddit))
8
9 return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
10 .then(
11 response => response.json(),
12 )
13 .then(json =>
14 dispatch(FETCH_SUCCESS))
15 )
16 }
17}
18
19
20//reducer
21const reducer = function(oldState, action) {
22 switch(action.type) {
23 case FETCH_SUCCESS :
24 return successState;
25 case FETCH_FAILED :
26 return errorState;
27 }
28}
29
30// store
31import { createStore, combineReducers, applyMiddleware } from 'redux';
32import thunk from 'redux-thunk';
33
34// applyMiddleware 爲 createStore 注入了 middleware:
35let store = createStore(reducer, applyMiddleware(thunk))複製代碼
看起來能夠解決異步問題了,而且也是 redux 的做者 Dan 親力親爲的做品,得力於官網的大力推薦,這個解決方案可謂帶着光環出生,但,記得開頭所說嗎
咱們在上述的探索中其實一直圍繞着一個根本的問題,反作用應該在哪裏處理?
thunk 的作法雖然很討巧,可是將反作用的處理過程分散到了 action creator,action 是用來描述動做是什麼,並不該該關心什麼條件下是什麼,他沒有這個職責,也不該該有這個職責。
另外,若是不一樣的業務須要在相同的fetch後處理不一樣的邏輯,就會產生不少的模板代碼,而且時序的控制如何處理這些很難解決,畢竟異步的操做沒辦法保證誰先發就誰先返回,這些痛點概括以下:
對於「找位置」的問題,redux-saga 提出了另外一種解決方案。
在聊方案以前,先說說 saga 是什麼?
這個翻譯和紅框是否是看蒙了?
大丈夫~,其實 saga 是爲了解決分佈式系統中的長時運行事務(LLT)的數據一致性的問題,出自康奈爾大學的一篇論文。連接
聽的更懵逼了吧,我試着舉個例子啊。
有一天 Mr.肖蓓泰 給他的女神求婚,女神說我最多10年給你答覆吧~。那麼若是女神10年內答應了肖蓓泰,就算成功了,不然就是失敗了。這就是 LLT。由於事務就是要麼全執行,要麼全不執行。可是肖蓓泰要是一直等10年是否是有點慘啊,
而後康奈爾大學的 2 位情感專家看不下去了,他們勸肖蓓泰,莫使金樽空對月啊,少年。你能夠答應等她(事務T1),而後去找其餘女神(釋放資源)。若是女神在10年內答應了,就結婚唄(事務 T2),這樣 LLT 就結束了,
若是等了十年女神拒絕了,也沒有其餘女神答應你,那就不等了(事務 T3 回滾 T1)。這幾個事務的組合就是一個 saga,固然,會有更多複雜的狀況。複製代碼
那這跟 redux-saga 有什麼關係呢?
redux-saga 是一個用於管理反作用的 redux 中間件,能夠想像爲,一個 saga 就像是應用程序中一個單獨的線程,它獨自負責處理反作用。 redux-saga
是一個 redux 中間件,意味着這個線程能夠經過正常的 redux action 從主應用程序啓動,暫停和取消,它能訪問完整的 redux state,也能夠 dispatch redux action。
個人理解:一個 sage 就是一組 行爲 generator 的操做集合(事務),不一樣的 saga 能夠組合(LLT 由小事務組成),redux-sage 來統一調度他們。
你也能夠經過 Background on the Saga concept 來產生本身的理解。
回到上面的的 thunk,咱們的代碼用 saga 會實現成下面的樣子:
1function* fetchPosts() {
2 yield put( actions.requestPosts() )
3 const products = yield call(fetchApi, '/products')
4 yield put( actions.receivePosts(products) )
5}
6
7function* watchFetch() {
8 yield takeLatest('FETCH_POSTS', fetchPosts)
9}複製代碼
他的執行過程以下:
將反作用的處理抽離至 saga 層,action 維持原始的職責,對原始的 redux 流程侵入性很是低。各個模塊的職責也相對清晰。
因爲是使用 generator/iterator 來組織邏輯,擁有更爲靈活的流程控制。本文只探討他的設計理念,對自身包含的諸多的優勢再也不贅述,可是靈活每每意味着複雜,其衆多的 api 與抽象概念,對於初期開發者仍是學習曲線比較陡峭的。
好了,至此你們應該瞭解爲何 saga 成爲了異步處理的寵兒了,可是
但願你們能夠辯證地看待所謂的最佳實踐,用本身的理解去探索世界,好比:redux-observable 瞭解一下呢? 🤪