注:這篇是17年1月的文章,搬運自本人 blog...html
https://github.com/BuptStEve/...前端
在上一篇中介紹了 Redux 的各項基礎 api。接着一步一步地介紹如何與 React 進行結合,並從引入過程當中遇到的各個痛點引出 react-redux 的做用和原理。react
不過目前爲止還都是紙上談兵,在平常的開發中最多見異步操做(如經過 ajax、jsonp 等方法 獲取數據),在學習完上一篇後你可能依然沒有頭緒。所以本文將深刻淺出地對於 redux 的進階用法進行介紹。git
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. ———— by Dan Abramov
這是 redux 做者對 middleware 的描述,middleware 提供了一個分類處理 action 的機會,在 middleware 中你能夠檢閱每個流過的 action,挑選出特定類型的 action 進行相應操做,給你一次改變 action 的機會。es6
說得好像很吊...不過有啥用咧...?github
由於改變 store 的惟一方法就是 dispatch 一個 action,因此有時須要將每次 dispatch 操做都打印出來做爲操做日誌,這樣一來就能夠很容易地看出是哪一次 dispatch 致使了異常。ajax
const action = addTodo('Use Redux'); console.log('dispatching', action); store.dispatch(action); console.log('next state', store.getState());
顯然這種在每個 dispatch 操做的先後都手動加代碼的方法,簡直讓人不忍直視...數據庫
聰明的你必定立刻想到了,不如將上述代碼封裝成一個函數,而後直接調用該方法。express
function dispatchAndLog(store, action) { console.log('dispatching', action); store.dispatch(action); console.log('next state', store.getState()); } dispatchAndLog(store, addTodo('Use Redux'));
矮油,看起來不錯喲。編程
不過每次使用都須要導入這個額外的方法,一旦不想使用又要所有替換回去,好麻煩啊...
在此暫不探究爲啥叫猴子補丁而不是什麼其餘補丁。
簡單來講猴子補丁指的就是:以替換原函數的方式爲其添加新特性或修復 bug。
let next = store.dispatch; // 暫存原方法 store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action); let result = next(action); // 應用原方法 console.log('next state', store.getState()); return result; };
這樣一來咱們就「偷樑換柱」般的爲原 dispatch 添加了輸出日誌的功能。
目前看起來很不錯,然鵝假設咱們又要添加別的一箇中間件,那麼代碼中將會有重複的 let next = store.dispatch;
代碼。
對於這個問題咱們能夠經過參數傳遞,返回新的 dispatch 來解決。
function logger(store) { const next = store.dispatch; return function dispatchAndLog(action) { console.log('dispatching', action); const result = next(action); // 應用原方法 console.log('next state', store.getState()); return result; } } store.dispatch = logger(store); store.dispatch = anotherMiddleWare(store);
注意到最後應用中間件的代碼其實就是一個鏈式的過程,因此還能夠更進一步優化綁定中間件的過程。
function applyMiddlewareByMonkeypatching(store, middlewares) { // 由於傳入的是原對象引用的值,slice 方法會生成一份拷貝, // 因此以後調用的 reverse 方法不會改變原數組 middlewares = middlewares.slice(); // 咱們但願按照數組本來的前後順序觸發各個中間件, // 因此最後的中間件應當最接近本來的 dispatch, // 就像洋蔥同樣一層一層地包裹原 dispatch middlewares.reverse(); // 在每個 middleware 中變換 store.dispatch 方法。 middlewares.forEach((middleware) => store.dispatch = middleware(store); ); } // 先觸發 logger,再觸發 anotherMiddleWare 中間件(相似於 koa 的中間件機制) applyMiddlewareByMonkeypatching(store, [ logger, anotherMiddleWare ]);
so far so good~! 如今不只隱藏了顯式地緩存原 dispatch 的代碼,並且調用起來也很優雅~,然鵝這樣就夠了麼?
注意到,以上寫法仍然是經過 store.dispatch = middleware(store);
改寫原方法,並在中間件內部經過 const next = store.dispatch;
讀取當前最新的方法。
本質上其實仍是 monkey patch,只不過將其封裝在了內部,不過如果將 dispatch 方法經過參數傳遞進來,這樣在 applyMiddleware 函數中就能夠暫存 store.dispatch(而不是一次又一次的改寫),豈不美哉?
// 經過參數傳遞 function logger(store, next) { return function dispatchAndLog(action) { // ... } } function applyMiddleware(store, middlewares) { // ... // 暫存原方法 let dispatch = store.dispatch; // middleware 中經過閉包獲取 dispatch,而且更新 dispatch middlewares.forEach((middleware) => dispatch = middleware(store, dispatch); ); }
接着應用函數式編程的 curry 化(一種使用匿名單參數函數來實現多參數函數的方法。),還能夠再進一步優化。(實際上是爲了使用 compose 將中間件函數先組合再綁定)
function logger(store) { return function(next) { return function(action) { console.log('dispatching', action); const result = next(action); // 應用原方法 console.log('next state', store.getState()); return result; } } } // -- 使用 es6 的箭頭函數可讓代碼更加優雅更函數式... -- const logger = (store) => (next) => (action) => { console.log('dispatching', action); const result = next(action); // 應用原方法 console.log('next state', store.getState()); return result; }; function applyMiddleware(store, middlewares) { // ... let dispatch = store.dispatch; middlewares.forEach((middleware) => dispatch = middleware(store)(dispatch); // 注意調用了兩次 ); // ... }
以上方法離 Redux 中最終的 applyMiddleware 實現已經很接近了,
在 Redux 的最終實現中,並無採用咱們以前的 slice + reverse
的方法來倒着綁定中間件。而是採用了 map + compose + reduce
的方法。
先來講這個 compose 函數,在數學中如下等式十分的天然。
f(g(x)) = (f o g)(x)
f(g(h(x))) = (f o g o h)(x)
用代碼來表示這一過程就是這樣。
// 傳入參數爲函數數組 function compose(...funcs) { // 返回一個閉包, // 將右邊的函數做爲內層函數執行,並將執行結果做爲外層函數再次執行 return funcs.reduce((a, b) => (...args) => a(b(...args))); }
不瞭解 reduce 函數的人可能對於以上代碼會感到有些費解,舉個栗子來講,有函數數組 [f, g, h]傳入 compose 函數執行。
(...args) => f(g(...args))
a
,而參數 b 是 h
h(...args)
做爲參數傳入 a,即最後返回的仍是一個函數 (...args) => f(g(h(...args)))
所以最終版 applyMiddleware 實現中並不是依次執行綁定,而是採用函數式的思惟,將做用於 dispatch 的函數首先進行組合,再進行綁定。(因此要中間件要 curry 化)
// 傳入中間件函數的數組 function applyMiddleware(...middlewares) { // 返回一個函數的緣由在 createStore 部分再進行介紹 return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] // 保存綁定了 middlewareAPI 後的函數數組 const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) // 使用 compose 函數按照從右向左的順序綁定(執行順序是從左往右) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } // store -> { getState } 從傳遞整個 store 改成傳遞部分 api const logger = ({ getState }) => (next) => (action) => { console.log('dispatching', action); const result = next(action); // 應用原方法 console.log('next state', getState()); return result; };
綜上以下圖所示整個中間件的執行順序是相似於洋蔥同樣首先按照從外到內的順序執行 dispatch 以前的中間件代碼,在 dispatch(洋蔥的心)執行後又反過來,按照從內到左外的順序執行 dispatch 以後的中間件代碼。
橋都麻袋!
你真的都理解了麼?
const logger = (store) => (next) => (action) => { console.log('dispatching', action); // 調用原始 dispatch,而不是上一個中間件傳進來的 const result = store.dispatch(action); // <- 這裏 console.log('next state', store.getState()); return result; };
正常狀況下,如圖左,當咱們 dispatch 一個 action 時,middleware 經過 next(action) 一層一層處理和傳遞 action 直到 redux 原生的 dispatch。若是某個 middleware 使用 store.dispatch(action) 來分發 action,就發生了右圖的狀況,至關於從外層從新來一遍,假如這個 middleware 一直簡單粗暴地調用 store.dispatch(action),就會造成無限循環了。(其實就至關於猴子補丁沒補上,不停地調用原來的函數)
所以最終版裏不是直接傳遞 store,而是傳遞 getState 和 dispatch,傳遞 getState 的緣由是能夠經過 getState 獲取當前狀態。而且還將 dispatch 用一個匿名函數包裹 dispatch: (action) => dispatch(action)
,這樣不但能夠防止 dispatch 被中間件修改,並且只要 dispatch 更新了,middlewareAPI 中的 dispatch 也會隨之發生變化。
在上一篇中咱們使用 createStore 方法只用到了它前兩個參數,即 reducer 和 preloadedState,然鵝其實它還擁有第三個參數 enhancer。
enhancer 參數能夠實現中間件、時間旅行、持久化等功能,Redux 僅提供了 applyMiddleware 用於應用中間件(就是 1.6. 中的那個)。
在平常使用中,要應用中間件能夠這麼寫。
import { createStore, combineReducers, applyMiddleware, } from 'redux'; // 組合 reducer const rootReducer = combineReducers({ todos: todosReducer, filter: filterReducer, }); // 中間件數組 const middlewares = [logger, anotherMiddleWare]; const store = createStore( rootReducer, initialState, applyMiddleware(...middlewares), ); // 若是不須要 initialState 的話也能夠忽略 const store = createStore( rootReducer, applyMiddleware(...middlewares), );
在上文 applyMiddleware 的實現中留了個懸念,就是爲何返回的是一個函數,由於 enhancer 被定義爲一個高階函數,接收 createStore 函數做爲參數。
/** * 建立一個 redux store 用於保存狀態樹, * 惟一改變 store 中數據的方法就是對其調用 dispatch * * 在你的應用中應該只有一個 store,想要針對不一樣的部分狀態響應 action, * 你應該使用 combineReducers 將多個 reducer 合併。 * * @param {函數} reducer 很少解釋了 * @param {對象} preloadedState 主要用於先後端同構時的數據同步 * @param {函數} enhancer 很牛逼,能夠實現中間件、時間旅行,持久化等 * ※ Redux 僅提供 applyMiddleware 這個 Store Enhancer ※ * @return {Store} */ export default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } // enhancer 是一個高階函數,接收 createStore 函數做爲參數 return enhancer(createStore)(reducer, preloadedState) } // ... // 後續內容推薦看看參考資料部分的【Redux 莞式教程】 }
總的來講 Redux 有五個 API,分別是:
createStore 生成的 store 有四個 API,分別是:
以上 API 咱們還沒介紹的應該就剩 bindActionCreators 了。這個 API 其實就是個語法糖起了方便地給 action creator 綁定 dispatch 的做用。
// 通常寫法 function mapDispatchToProps(dispatch) { return { onPlusClick: () => dispatch(increment()), onMinusClick: () => dispatch(decrement()), }; } // 使用 bindActionCreators import { bindActionCreators } from 'redux'; function mapDispatchToProps(dispatch) { return bindActionCreators({ onPlusClick: increment, onMinusClick: decrement, // 還能夠綁定更多函數... }, dispatch); } // 甚至若是定義的函數輸入都相同的話還能更加簡潔 export default connect( mapStateToProps, // 直接傳一個對象,connect 自動幫你綁定 dispatch { onPlusClick: increment, onMinusClick: decrement }, )(App);
下面讓咱們告別乾淨的同步世界,進入「骯髒」的異步世界~。
在函數式編程中,異步操做、修改全局變量等與函數外部環境發生的交互叫作反作用(Side Effect)
一般認爲這些操做是邪惡(evil)骯髒(dirty)的,而且也是致使 bug 的源頭。
由於與之相對的是純函數(pure function),即對於一樣的輸入老是返回一樣的輸出的函數,使用這樣的函數很容易作組合、測試等操做,很容易驗證和保證其正確性。(它們就像數學公式通常準確)
如今有這麼一個顯示通知的應用場景,在通知顯示後5秒鐘隱藏該通知。
首先固然是編寫 action
最直觀的寫法就是首先顯示通知,而後使用 setTimeout 在5秒後隱藏通知。
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' }); setTimeout(() => { store.dispatch({ type: 'HIDE_NOTIFICATION' }); }, 5000);
然鵝,通常在組件中尤爲是展現組件中無法也不必獲取 store,所以通常將其包裝成 action creator。
// actions.js export function showNotification(text) { return { type: 'SHOW_NOTIFICATION', text }; } export function hideNotification() { return { type: 'HIDE_NOTIFICATION' }; } // component.js import { showNotification, hideNotification } from '../actions'; this.props.dispatch(showNotification('You just logged in.')); setTimeout(() => { this.props.dispatch(hideNotification()); }, 5000);
或者更進一步地先使用 connect 方法包裝。
this.props.showNotification('You just logged in.'); setTimeout(() => { this.props.hideNotification(); }, 5000);
到目前爲止,咱們沒有用任何 middleware 或者別的概念。
上一種直觀寫法有一些問題
因此爲了解決以上問題,咱們能夠爲通知加上 id,並將顯示和消失的代碼包起來。
// actions.js const showNotification = (text, id) => ({ type: 'SHOW_NOTIFICATION', id, text, }); const hideNotification = (id) => ({ type: 'HIDE_NOTIFICATION', id, }); let nextNotificationId = 0; export function showNotificationWithTimeout(dispatch, text) { const id = nextNotificationId++; dispatch(showNotification(id, text)); setTimeout(() => { dispatch(hideNotification(id)); }, 5000); } // component.js showNotificationWithTimeout(this.props.dispatch, 'You just logged in.'); // otherComponent.js showNotificationWithTimeout(this.props.dispatch, 'You just logged out.');
爲啥 showNotificationWithTimeout
函數要接收 dispatch
做爲第一個參數呢?
雖然一般一個組件都擁有觸發 dispatch 的權限,可是如今咱們想讓一個外部函數(showNotificationWithTimeout)來觸發 dispatch,因此須要將 dispatch 做爲參數傳入。
可能你會說若是有一個從其餘模塊中導出的單例 store,那麼是否是一樣也能夠不傳遞 dispatch 以上代碼也能夠這樣寫。
// store.js export default createStore(reducer); // actions.js import store from './store'; // ... let nextNotificationId = 0; export function showNotificationWithTimeout(text) { const id = nextNotificationId++; store.dispatch(showNotification(id, text)); setTimeout(() => { store.dispatch(hideNotification(id)); }, 5000); } // component.js showNotificationWithTimeout('You just logged in.'); // otherComponent.js showNotificationWithTimeout('You just logged out.');
這樣看起來彷佛更簡單一些,不過牆裂不推薦這樣的寫法。主要的緣由是這樣的寫法強制讓 store 成爲一個單例。這樣一來要實現服務器端渲染(Server Rendering)將十分困難。由於在服務端,爲了讓不一樣的用戶獲得不一樣的預先獲取的數據,你須要讓每個請求都有本身的 store。
而且單例 store 也將讓測試變得困難。當測試 action creator 時你將沒法本身模擬一個 store,由於它們都引用了從外部導入的那個特定的 store,因此你甚至沒法從外部重置狀態。
首先聲明 redux-thunk 這種方案對於小型的應用來講足夠平常使用,然鵝對於大型應用來講,你可能會發現一些不方便的地方。(例如對於 action 須要組合、取消、競爭等複雜操做的場景)
首先來明確什麼是 thunk...
A thunk is a function that wraps an expression to delay its evaluation.
簡單來講 thunk 就是封裝了表達式的函數,目的是延遲執行該表達式。不過有啥應用場景呢?
目前爲止,在上文中的 2.1.2. 異步 action creator 部分,最後得出的方案有如下明顯的缺點
此外,在平常使用時,咱們還須要區分哪些函數是同步的 action creator,那些是異步的 action creator。(異步的須要傳 dispatch...)
計將安出?
其實問題的本質在於 Redux 「有眼不識 function」,目前爲止 dispatch 函數接收的參數只能是 action creator 返回的普通的 action。因此若是咱們讓 dispatch 對於 function 網開一面,走走後門潛規則一下不就行啦~
實現方式很簡單,想一想第一節介紹的爲 dispatch 添加日誌功能的過程。
// redux-thunk 源碼 function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
以上就是 redux-thunk 的源碼,就是這麼簡單,判斷下若是傳入的 action 是函數的話,就執行這個函數...(withExtraArgument 是爲了添加額外的參數,詳情見 redux-thunk 的 README.md)
添加了 redux-thunk 中間件後代碼能夠這麼寫。
// actions.js // ... let nextNotificationId = 0; export function showNotificationWithTimeout(text) { // 返回一個函數 return function(dispatch) { const id = nextNotificationId++; dispatch(showNotification(id, text)); setTimeout(() => { dispatch(hideNotification(id)); }, 5000); }; } // component.js 像同步函數同樣的寫法 this.props.dispatch(showNotificationWithTimeout('You just logged in.')); // 或者 connect 後直接調用 this.props.showNotificationWithTimeout('You just logged in.');
目前咱們對於簡單的延時異步操做的處理已經瞭然於胸了,如今讓咱們來考慮一下經過 ajax 或 jsonp 等接口來獲取數據的異步場景。
很天然的,咱們會發起一個請求,而後等待請求的響應(請求可能成功或是失敗)。
即有基本的三種狀態和與之對應的 action:
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
按照這個思路,舉一個簡單的栗子。
// Constants const FETCH_POSTS_REQUEST = 'FETCH_POSTS_REQUEST'; const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS'; const FETCH_POSTS_FAILURE = 'FETCH_POSTS_FAILURE'; // Actions const requestPosts = (id) => ({ type: FETCH_POSTS_REQUEST, payload: id, }); const receivePosts = (res) => ({ type: FETCH_POSTS_SUCCESS, payload: res, }); const catchPosts = (err) => ({ type: FETCH_POSTS_FAILURE, payload: err, }); const fetchPosts = (id) => (dispatch, getState) => { dispatch(requestPosts(id)); return api.getData(id) .then(res => dispatch(receivePosts(res))) .catch(error => dispatch(catchPosts(error))); }; // reducer const reducer = (oldState, action) => { switch (action.type) { case FETCH_POSTS_REQUEST: return requestState; case FETCH_POSTS_SUCCESS: return successState; case FETCH_POSTS_FAILURE: return errorState; default: return oldState; } };
儘管這已是最簡單的調用接口場景,咱們甚至還沒寫一行業務邏輯代碼,但講道理的話代碼仍是比較繁瑣的。
並且其實代碼是有必定的「套路」的,好比其實整個代碼都是針對請求、成功、失敗三部分來處理的,這讓咱們天然聯想到 Promise,一樣也是分爲 pending、fulfilled、rejected 三種狀態。
那麼這二者能夠結合起來讓模版代碼精簡一下麼?
首先開門見山地使用 redux-promise 中間件來改寫以前的代碼看看效果。
// Constants const FETCH_POSTS_REQUEST = 'FETCH_POSTS_REQUEST'; // Actions const fetchPosts = (id) => ({ type: FETCH_POSTS_REQUEST, payload: api.getData(id), // payload 爲 Promise 對象 }); // reducer const reducer = (oldState, action) => { switch (action.type) { case FETCH_POSTS_REQUEST: // requestState 被「吃掉」了 // 而成功、失敗的狀態經過 status 來判斷 if (action.status === 'success') { return successState; } else { return errorState; } default: return oldState; } };
能夠看出 redux-promise 中間件比較激進、比較原教旨。
不但將發起請求的初始狀態被攔截了(緣由見下文源碼),並且使用 action.status 而不是 action.type 來區分兩個 action 這一作法也值得商榷(我的傾向使用 action.type 來判斷)。
// redux-promise 源碼 import { isFSA } from 'flux-standard-action'; function isPromise(val) { return val && typeof val.then === 'function'; } export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) // 直接調用 Promise.then(因此發不出請求開始的 action) ? action.payload.then( // 自動 dispatch result => dispatch({ ...action, payload: result }), // 自動 dispatch error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); } ) : next(action); }; }
以上是 redux-promise 的源碼,十分簡單。主要邏輯是判斷若是是 Promise 就執行 then 方法。此外還根據是否是 FSA 決定調用的是 action 自己仍是 action.payload 而且對於 FSA 會自動 dispatch 成功和失敗的 FSA。
儘管 redux-promise 中間件節省了大量代碼,然鵝它的缺點除了攔截請求開始的 action,以及使用 action.status 來判斷成功失敗狀態之外,還有就是由此引伸出的一個沒法實現的場景————樂觀更新(Optimistic Update)。
樂觀更新比較直觀的栗子就是在微信、QQ等通信軟件中,發送的消息當即在對話窗口中展現,若是發送失敗了,在消息旁邊展現提示便可。因爲在這種交互方式中「樂觀」地相信操做會成功,所以稱做樂觀更新。
由於樂觀更新發生在用戶發起操做時,因此要實現它,意味着必須有表示用戶初始動做的 action。
所以爲了解決這些問題,相對於比較原教旨的 redux-promise 來講,更加溫和派一點的 redux-promise-middleware 中間件應運而生。先看看代碼怎麼說。
// Constants const FETCH_POSTS = 'FETCH_POSTS'; // 前綴 // Actions const fetchPosts = (id) => ({ type: FETCH_POSTS, // 傳遞的是前綴,中間件會自動生成中間狀態 payload: { promise: api.getData(id), data: id, }, }); // reducer const reducer = (oldState, action) => { switch (action.type) { case `${FETCH_POSTS}_PENDING`: return requestState; // 可經過 action.payload.data 獲取 id case `${FETCH_POSTS}_FULFILLED`: return successState; case `${FETCH_POSTS}_REJECTED`: return errorState; default: return oldState; } };
若是不須要樂觀更新,fetchPosts 函數能夠更加簡潔。
// 此時初始 actionGET_DATA_PENDING 仍然會觸發,可是 payload 爲空。 const fetchPosts = (id) => ({ type: FETCH_POSTS, // 傳遞的是前綴 payload: api.getData(id), // 等價於 payload: { promise: api.getData(id) }, });
相對於 redux-promise 簡單粗暴地直接過濾初始 action,從 reducer 能夠看出,redux-promise-middleware 會首先自動觸發一個 FETCH_POSTS_PENDING 的 action,以此保留樂觀更新的能力。
而且,在狀態的區分上,迴歸了經過 action.type 來判斷狀態的「正途」,其中 _PENDING
、_FULFILLED
、_REJECTED
後綴借用了 Promise 規範 (固然它們是可配置的) 。
後綴能夠配置全局或局部生效,例如全局配置能夠這麼寫。
applyMiddleware( promiseMiddleware({ promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR'] }) )
源碼地址點我,相似 redux-promise 也是在中間件中攔截了 payload 中有 Promise 的 action,並主動 dispatch 三種狀態的 action,註釋也很詳細在此就不贅述了。
注意:redux-promise、redux-promise-middleware 與 redux-thunk 之間並非互相替代的關係,而更像一種補充優化。
簡單小結一下,Redux 的數據流以下所示:
UI => action => action creator => reducer => store => react => v-dom => UI
redux-thunk 的思路是保持 action 和 reducer 簡單純粹,然鵝反作用操做(在前端主要體如今異步操做上)的複雜度是不可避免的,所以它將其放在了 action creator 步驟,經過 thunk 函數手動控制每一次的 dispatch。
redux-promise 和 redux-promise-middleware 只是在其基礎上作一些輔助性的加強,處理異步的邏輯本質上是相同的,即將維護複雜異步操做的責任推到了用戶的身上。
這種實現方式當然很好理解,並且理論上能夠應付全部異步場景,可是由此帶來的問題就是模版代碼太多,一旦流程複雜那麼異步代碼就會處處都是,很容易致使出現 bug。
所以有一些其餘的中間件,例如 redux-loop 就將異步處理邏輯放在 reducer 中。(Redux 的思想借鑑了 Elm,注意並非「餓了麼」,而 Elm 就是將異步處理放在 update(reducer) 層中)。
Synchronous state transitions caused by returning a new state from the reducer in response to an action are just one of all possible effects an action can have on application state.
這種經過響應一個 action,在 reducer 中返回一個新 state,從而引發同步狀態轉換的方式,只是在應用狀態中一個 action 能擁有的全部可能影響的一種。(可能沒翻好~歡迎勘誤~)
redux-loop 認爲許多其餘的處理異步的中間件,尤爲是經過 action creator 方式實現的中間件,錯誤地讓用戶認爲異步操做從根本上與同步操做並不相同。這樣一來無形中鼓勵了中間件以許多特殊的方式來處理異步狀態。
與之相反,redux-loop 專一於讓 reducer 變得足夠強大以便處理同步和異步操做。在具體實現上 reducer 不只可以根據特定的 action 決定當前的轉換狀態,並且還能決定接着發生的操做。
應用中全部行爲均可以在一個地方(reducer)中被追蹤,而且這些行爲能夠輕易地分割和組合。(redux 做者 Dan 開了個至今依然 open 的 issue:Reducer Composition with Effects in JavaScript,討論關於對 reducer 進行分割組合的問題。)
redux-loop 模仿 Elm 的模式,引入了 Effect 的概念,在 reducer 中對於異步等操做使用 Effect 來處理。以下官方示例所示:
import { Effects, loop } from 'redux-loop'; function fetchData(id) { return fetch(`endpoint/${id}`) .then((r) => r.json()) .then((data) => ({ type: 'FETCH_SUCCESS', payload: data })) .catch((error) => ({ type: 'FETCH_FAILURE', payload: error.message })); } function reducer(state, action) { switch(action.type) { case 'FETCH_START': return loop( // <- 並無直接返回 state,實際上了返回數組 [state, effect] { ...state, loading: true }, Effects.promise(fetchData, action.payload.id) ); case 'FETCH_SUCCESS': return { ...state, loading: false, data: action.payload }; case 'FETCH_FAILURE': return { ...state, loading: false, errorMessage: action.payload }; } }
雖然這個想法很 Elm 很函數式,不過因爲修改了 reducer 的返回類型,這樣一來會致使許多已有的 Api 和第三方庫沒法使用,甚至連 redux 庫中的 combineReducers 方法都須要使用 redux-loop 提供的定製版本。所以這也是 redux-loop 最終沒法轉正的緣由:
"If a solution doesn’t work with vanilla combineReducers(), it won’t get into Redux core."
讓咱們的思路從新回到通知的場景,以前的代碼實現了:
如今假設可親可愛的產品又提出了新需求:
「這個實現不了...」(全文完)
這個固然能夠實現,只不過若是隻用以前的 redux-thunk 實現起來會很麻煩。例如能夠在 store 中增長兩個數組分別表示當前展現列表和等待隊列,而後在 reducer 中手動控制各個狀態時這倆數組的變化。
首先來看看使用了 redux-saga 後代碼會變成怎樣~(代碼來自生產環境的某 app)
function* toastSaga() { const MaxToasts = 3; const ToastDisplayTime = 4000; let pendingToasts = []; // 等待隊列 let activeToasts = []; // 展現列表 function* displayToast(toast) { if ( activeToasts >= MaxToasts ) { throw new Error("can't display more than " + MaxToasts + " at the same time"); } activeToasts = [...activeToasts, toast]; // 新增通知到展現列表 yield put(events.toastDisplayed(toast)); // 展現通知 yield call(delay, ToastDisplayTime); // 通知的展現時間 yield put(events.toastHidden(toast)); // 隱藏通知 activeToasts = _.without(activeToasts,toast); // 從展現列表中刪除 } function* toastRequestsWatcher() { while (true) { const event = yield take(Names.TOAST_DISPLAY_REQUESTED); // 監聽通知展現請求 const newToast = event.data.toastData; pendingToasts = [...pendingToasts, newToast]; // 將新通知放入等待隊列 } } function* toastScheduler() { while (true) { if (activeToasts.length < MaxToasts && pendingToasts.length > 0) { const [firstToast,...remainingToasts] = pendingToasts; pendingToasts = remainingToasts; yield fork(displayToast, firstToast); // 取出隊頭的通知進行展現 // 增長一點延遲,這樣一來兩個併發的通知請求不會同時展現 yield call(delay, 300); } else { yield call(delay, 50); } } } yield [ call(toastRequestsWatcher), call(toastScheduler) ] } // reducer const reducer = (state = {toasts: []}, event) => { switch (event.name) { case Names.TOAST_DISPLAYED: return { ...state, toasts: [...state.toasts, event.data.toastData] }; case Names.TOAST_HIDDEN: return { ...state, toasts: _.without(state.toasts, event.data.toastData) }; default: return state; } };
先不要在乎代碼的細節,簡單分析一下上述代碼的邏輯:
排隊等具體的業務邏輯都放到了 toastSaga 函數中
基於這樣邏輯分離的寫法,還能夠繼續知足更加複雜的需求:
redux-saga V.S. redux-thunk[[11]]
redux-saga 的優勢:
redux-saga 的缺點:
通知場景各類中間件寫法的完整代碼能夠看這裏
Sagas 的概念來源於這篇論文,該論文從數據庫的角度談了 Saga Pattern。
Saga 就是可以知足特定條件的長事務(Long Lived Transaction)
暫且不提這個特定條件是什麼,首先通常學過數據庫的都知道事務(Transaction)是啥~
若是不知道的話能夠用轉帳來理解,A 轉給 B 100 塊錢的操做須要保證完成 A 先減 100 塊錢而後 B 加 100 塊錢這兩個操做,這樣才能保證轉帳先後 A 和 B 的存款總額不變。
若是在給 B 加 100 塊錢的過程當中發生了異常,那麼就要返回轉帳前的狀態,即給 A 再加上以前減的 100 塊錢(否則錢就不知去向了),這樣的一次轉帳(要麼轉成功,要麼失敗返回轉帳前的狀態)就是一個事務。
長事務顧名思義就是一個長時間的事務。
通常來講是經過給正在進行事務操做的對象加鎖,來保證事務併發時不會出錯。
例如 A 和 B 都給 C 轉 100 塊錢。
以押尾光太郎的指彈演奏會售票舉例,在一個售票的時間段後,最終舉辦方須要肯定售票數量,這就是一個長事務。
然鵝,對於長事務來講總不能一直鎖住對應數據吧?
爲了解決這個問題,假設一個長事務:T,
能夠被拆分紅許多相互獨立的子事務(subtransaction):t_1 ~ t_n。
以上述押尾桑的表演爲例,每一個
t
就是一筆售票記錄。
假如每次購票都一次成功,且沒有退票的話,整個流程就以下圖通常被正常地執行。
那假若有某次購票失敗了怎麼辦?
A LLT is a saga if it can be written as a sequence of transactions that can be interleaved with other transactions.
Saga 就是可以被寫成事務的序列,而且可以在執行過程當中被其餘事務插入執行的長事務。
Saga 經過引入補償事務(Compensating Transaction)的概念,解決事務失敗的問題。
即任何一個 saga 中的子事務 t_i,都有一個補償事務 c_i 負責將其撤銷(undo)。
注意是撤銷該子事務,而不是回到子事務發生前的時間點。
根據以上邏輯,能夠推出很簡單的公式:
t_1, t_2, t_3, ..., t_n
t_1, t_2, t_3, ..., t_n, c_n, ..., c_1
注意到圖中的 c_4 其實並無必要,不過由於每次撤銷執行都應該是冪等(Idempotent)的,因此也不會出錯。
篇幅有限在此就不繼續深刻介紹...
redux-saga 中間件基於 Sagas 的理論,經過監聽 action,生成對應的各類子 saga(子事務)解決了複雜異步問題。
而接下來要介紹的 redux-observable 中間件背後的理論是響應式編程(Reactive Programming)。
In computing, reactive programming is a programming paradigm oriented around data flows and the propagation of change.
簡單來講,響應式編程是針對異步數據流的編程而且認爲:萬物皆流(Everything is Stream)。
流(Stream)就是隨着時間的流逝而發生的一系列事件。
例如點擊事件的示意圖就是這樣。
用字符表示【上上下下左右左右BABA】能夠像這樣。(注意順序是從左往右)
--上----上-下---下----左---右-B--A-B--A---X-|-> 上, 下, 左, 右, B, A 是數據流發射的值 X 是數據流發射的錯誤 | 是完成信號 ---> 是時間線
那麼咱們要根據一個點擊流來計算點擊次數的話能夠這樣。(通常響應式編程庫都會提供許多輔助方法如 map、filter、scan 等)
clickStream: ---c----c--c----c------c--> map(c becomes 1) ---1----1--1----1------1--> scan(+) counterStream: ---1----2--3----4------5-->
如上所示,原始的 clickStream 通過 map 後產生了一個新的流(注意原始流不變),再對該流進行 scan(+) 的操做就生成了最終的 counterStream。
再來個栗子~,假設咱們須要從點擊流中獲得關於雙擊的流(250ms 之內),而且對於大於兩次的點擊也認爲是雙擊。先想想應該怎麼用傳統的命令式、狀態式的方式來寫,而後再想一想用流的思考方式又會是怎麼樣的~。
這裏咱們用瞭如下輔助方法:
更多內容請繼續學習 RxJS。
redux-observable 就是一個使用 RxJS 監聽每一個 action 並將其變成可觀測流(observable stream)的中間件。
其中最核心的概念叫作 epic,就是一個監聽流上 action 的函數,這個函數在接收 action 並進行一些操做後能夠再返回新的 action。
At the highest level, epics are 「actions in, actions out」
redux-observable 經過在後臺執行 .subscribe(store.dispatch)
實現監聽。
Epic 像 Saga 同樣也是 Long Lived,即在應用初始化時啓動,持續運行到應用關閉。雖然 redux-observable 是一箇中間件,可是相似於 redux-saga,能夠想象它就像新開的進/線程,監聽着 action。
在這個運行流程中,epic 不像 thunk 同樣攔截 action,或阻止、改變任何本來 redux 的生命週期的其餘東西。這意味着每一個 dispatch 的 action 總會通過 reducer 處理,實際上在 epic 監聽到 action 前,action 已經被 reducer 處理過了。
因此 epic 的功能就是監聽全部的 action,過濾出須要被監聽的部分,對其執行一些帶反作用的異步操做,而後根據你的須要能夠再發射一些新的 action。
舉個自動保存的栗子,界面上有一個輸入框,每次用戶輸入了數據後,去抖動後進行自動保存,並在向服務器發送請求的過程當中顯示正在保存的 UI,最後顯示成功或失敗的 UI。
使用 redux-observable 中間件編寫代碼,能夠僅用十幾行關鍵代碼就實現上述功能。
import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/dom/ajax'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/startWith'; import { isSaving, savingSuccess, savingError, } from '../actions/autosave-actions.js'; const saveField = (action$) => // 通常在變量後面加 $ 表示是個 stream action$ .ofType('SAVE_FIELD') // 使用 ofType 監聽 'SAVE_FIELD' action .debounceTime(500) // 防抖動 // 即 map + mergeAll 由於異步致使 map 後有多個流須要 merge .mergeMap(({ payload }) => Observable.ajax({ // 發起請求 method: 'PATCH', url: payload.url, body: JSON.stringify(payload), }) .map(res => savingSuccess(res)) // 發出成功的 action .catch(err => Observable.of(savingError(err))) // 捕捉錯誤併發出 action .startWith(isSaving()) // 發出請求開始的 action ); export default saveField;
篇幅有限在此就不繼續深刻介紹...
若是以爲看視頻聽英語麻煩的話知乎有人翻譯了...
本文從爲 Redux 應用添加日誌功能(記錄每一次的 dispatch)入手,引出 redux 的中間件(middleware)的概念和實現方法。
接着從最簡單的 setTimeout 的異步操做開始,經過對比各類實現方法引出 redux 最基礎的異步中間件 redux-thunk。
針對 redux-thunk 使用時模版代碼過多的問題,又介紹了用於優化的 redux-promise 和 redux-promise-middleware 兩款中間件。
因爲本質上以上中間件都是基於 thunk 的機制來解決異步問題,因此不可避免地將維護異步狀態的責任推給了開發者,而且也由於難以測試的緣由。在複雜的異步場景下使用起來不免力不從心,容易出現 bug。
因此還簡單介紹了一下將處理反作用的步驟放到 reducer 中並經過 Effect 進行解決的 redux-loop 中間件。然鵝由於其沒法使用官方 combineReducers 的緣由而沒法被歸入 redux 核心代碼中。
此外社區根據 Saga 的概念,利用 ES6 的 generator 實現了 redux-saga 中間件。雖然經過 saga 函數將業務代碼分離,而且能夠用同步的方式流程清晰地編寫異步代碼,可是較多的新概念和 generator 的語法可能讓部分開發者望而卻步。
一樣是基於觀察者模式,經過監聽 action 來處理異步操做的 redux-observable 中間件,背後的思想是響應式編程(Reactive Programming)。相似於 saga,該中間件提出了 epic 的概念來處理反作用。即監聽 action 流,一旦監聽到目標 action,就處理相關反作用,而且還能夠在處理後再發射新的 action,繼續進行處理。儘管在處理異步流程時一樣十分方便,但對於開發者的要求一樣很高,須要開發者學習關於函數式的相關理論。
以上 to be continued...