上次咱們已經寫了 實現一個迷你Redux(基礎版) ,此次咱們來繼續完善Redux,繼續上篇的例子續寫。javascript
Redux 有個 API 是 applyMiddleware, 專門用來使用中間件的,首先咱們得知道,它用來幹嗎的。java
假設咱們如今須要記錄每次的 dispatch 先後 state 的記錄, 那要怎麼作呢?因而,簡單粗暴的在第一個 dispatch 方法先後加代碼react
console.log('prev state', store.getState()) console.log(action) store.dispatch({ type: 'INCREMENT' }) console.log('next state', store.getState())
這部分運行結果:git
prev state {value: 10} {type: "INCREMENT"} 當前數字爲:11 next state {value: 11}
但加完發現狀況不對,頁面有多個 dispatch 的話,要這樣寫不少次,會產生大量重複代碼。忽然,又要加需求了,須要記錄每次出錯的緣由,單獨的功能要求以下:github
try{ store.dispatch(action) }catch(err){ console.error('錯誤信息: ', err) }
而後兩個需求都要,那就湊合兩個,但疊一塊兒看更亂了。redux
顯然,咱們不能經過這種方式來作。比較理想的方案是Redux自己提供一個功能入口,讓咱們能夠在外面添加功能進去,這樣代碼就不會複雜。segmentfault
但若是給咱們現有實現的Redux添加功能,在哪一個環節添加比較合適呢?數組
咱們發現,以上需求都是和 dispatch 相關,只有發送 action 的這個步驟,即 store.dispatch() 方法,能夠添加功能。好比添加日誌功能,咱們只要把日誌放進 dispatch 函數裏,不就行了嗎,咱們只須要改造 dispatch 函數,把 dispatch 進行一層封裝。app
const store = createStore(counter) const next = store.dispatch store.dispatch = (action) => { try{ console.log('prev state', store.getState()) console.log(action) next(action) console.log('next state', store.getState()) }catch(err){ console.error('錯誤信息: ', err) } }
上面代碼,對 store.dispatch 進行了從新定義,這就是中間件的雛形。函數
因此說Redux的中間件就是一個函數,是對 dispatch 方法的擴展,加強 dispatch 的功能。
對於上述 dispatch 的封裝,其實是缺陷很大的。萬一又來 n 多個需求怎麼辦? 那 dispatch 函數就混亂到沒法維護了,故須要擴展性強的多中間件合做模式。
const store = createStore(counter) const next = store.dispatch const loggerMiddleware = (action) => { console.log('prev state', store.getState()) console.log(action) next(action) console.log('next state', store.getState()) } store.dispatch = (action) => { try { loggerMiddleware(action) } catch (err) { console.error('錯誤信息: ', err) } }
const exceptionMiddleware = (action) => { try { loggerMiddleware(action) } catch (err) { console.error('錯誤信息: ', err) } } store.dispatch = exceptionMiddleware
const exceptionMiddleware = (next) => (action) => { try { // loggerMiddleware(action) next(action) } catch (err) { console.error('錯誤信息: ', err) } }
這個寫法可能剛開始看不太適應,實際就是函數裏面,返回一個函數,即等效於
const exceptionMiddleware = function (next) { return function (action) { try { // loggerMiddleware(action) next(action) } catch (err) { console.error('錯誤信息: ', err) } } }
傳參數的時候便是exceptionMiddleware(next)(action)
const loggerMiddleware = (next) => (action) => { console.log('prev state', store.getState()) console.log(action) next(action) console.log('next state', store.getState()) }
目前爲止,整個中間件設計改造以下:
const store = createStore(counter) const next = store.dispatch const loggerMiddleware = (next) => (action) => { console.log('prev state', store.getState()) console.log(action) next(action) console.log('next state', store.getState()) } const exceptionMiddleware = (next) => (action) => { try { next(action) } catch (err) { console.error('錯誤信息: ', err) } } store.dispatch = exceptionMiddleware(loggerMiddleware(next))
const store = createStore(counter) const next = store.dispatch const loggerMiddleware = (store) => (next) => (action) => { console.log('prev state', store.getState()) console.log(action) next(action) console.log('next state', store.getState()) } const exceptionMiddleware = (store) => (next) => (action) => { try { next(action) } catch (err) { console.error('錯誤信息: ', err) } } const logger = loggerMiddleware(store) const exception = exceptionMiddleware(store) store.dispatch = exception(logger(next))
const timeMiddleware = (store) => (next) => (action) => { console.log('time', new Date().getTime()) next(action) } const logger = loggerMiddleware(store) const exception = exceptionMiddleware(store) const time = timeMiddleware(store) store.dispatch = exception(time(logger(next)))
上面的寫法可知,中間件的使用方式有點繁瑣,故咱們須要把細節封裝起來,經過擴展createStore來實現。
先來看看指望的用法:
/* 接收舊的 createStore,返回新的 createStore */ const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore); /* 返回了一個 dispatch 被重寫過的 store */ const store = newCreateStore(reducer);
export const applyMiddleware = function (...middlewares) { /* 返回一個重寫createStore的方法 */ return function rewriteCreateStoreFunc(oldCreateStore) { /* 返回重寫後新的 createStore */ return function newCreateStore(reducer, preloadedState) { // 生成 store const store = oldCreateStore(reducer, preloadedState) let dispatch = store.dispatch // 只暴露 store 部分給中間件用的API,而不傳入整個store const middlewareAPI = { getState: store.getState, dispatch: (action) => store.dispatch(action), } // 給每一箇中間件傳入API // 至關於 const logger = loggerMiddleware(store),即 const logger = loggerMiddleware({ getState, dispatch }) // const chain = [exception, time, logger] const chain = middlewares.map((middleware) => middleware(middlewareAPI)) // 實現 exception(time((logger(dispatch)))) chain.reverse().map((middleware) => { dispatch = middleware(dispatch) }) // 重寫dispatch store.dispatch = dispatch return store } } }
咱們來看這一處代碼:
chain.reverse().map((middleware) => { dispatch = middleware(dispatch) })
要注意一點,中間件是順序執行,可是 dispatch 倒是反序生成的。因此在這步會把數組順序給反序(好比 applyMiddleware(A, B, C),由於 A 在調用時須要知道 B 的 dispatch,B 在執行時須要知道 C 的 dispatch,那麼須要先知道 C 的 dispatch。)
官方Redux源碼,採用了 compose 函數,咱們也試試這種方式來寫:
export const applyMiddleware = (...middlewares) => { return (createStore) => (...args) => { // ... dispatch = compose(...chain)(store.dispatch) // ... } } // compose(fn1, fn2, fn3) // fn1(fn2(fn3)) // 從右到左來組合多個函數: 從右到左把接收到的函數合成後的最終函數 export const compose = (...funcs) => { if (funcs.length === 0) { return (arg) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((ret, item) => (...args) => ret(item(...args))) }
咱們再對代碼精簡:
export const applyMiddleware = (...middlewares) => { return (createStore) => (...args) => { const store = createStore(...args) let dispatch = store.dispatch const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action), } const chain = middlewares.map((middleware) => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch, } } } export const compose = (...funcs) => { if (funcs.length === 0) { return (arg) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((ret, item) => (...args) => ret(item(...args))) }
如今的問題是,有兩個 createStore 了,這怎麼區分,上篇咱們其實已經先告知了對中間件代碼處理,但具體怎麼推出的,咱們繼續看。
// 沒有中間件的 createStore const store = createStore(counter) // 有中間件的 createStore const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware); const newCreateStore = rewriteCreateStoreFunc(createStore); const store = newCreateStore(counter, preloadedState);
爲了讓用戶用起來統一一些,咱們能夠很簡單的使他們的使用方式一致,咱們修改下 createStore 方法
const createStore = (reducer, preloadedState, rewriteCreateStoreFunc) => { // 若是有 rewriteCreateStoreFunc,那就採用新的 createStore if(rewriteCreateStoreFunc){ const newCreateStore = rewriteCreateStoreFunc(createStore); return newCreateStore(reducer, preloadedState); } // ... }
不過Redux源碼 rewriteCreateStoreFunc 換了個名字,還加了判斷,也就是咱們上篇的代碼:
if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) }
因此中間件的用法爲
const store = createStore(counter, /* preloadedState可選 */ applyMiddleware(logger))
若是咱們作的項目很大,有大量 state,那麼維護起來很麻煩。Redux 提供了 combineReducers 這個方法,做用是把多個 reducer 合併成一個 reducer, 每一個 reducer 負責獨立的模塊。
咱們用一個新例子來舉例:
import { createStore, applyMiddleware, combineReducers } from 'redux' const initCounterState = { value: 10, } const initInfoState = { name: 'jacky', } const reducer = combineReducers({ counter: counterReducer, info: infoReducer, }) // counter reducer處理函數 function counterReducer(state = initCounterState, action) { switch (action.type) { case 'INCREMENT': return { ...state, value: state.value + 1, } case 'DECREMENT': return { ...state, value: state.value - 1, } default: return state } } function infoReducer(state = initInfoState, action) { switch (action.type) { case 'FULL_NAME': return { ...state, name: state.name + ' lin', } default: return state } } const store = createStore(reducer) const init = store.getState() // 一開始counter爲:10,info爲 jacky console.log(`一開始counter爲:${init.counter.value},info爲 ${init.info.name}`) function listener() { store.getState() } store.subscribe(listener) // 監聽state的改變 // counterReducer store.dispatch({ type: 'INCREMENT' }) store.dispatch({ type: 'INCREMENT' }) store.dispatch({ type: 'DECREMENT' }) // infoReducer store.dispatch({ type: 'FULL_NAME' }) // 執行完counter爲:11,info爲jacky lin console.log(`執行完counter爲:${store.getState().counter.value},info爲${store.getState().info.name}`) export default store
咱們來嘗試下如何實現這個 API,
首先要把一個函數裏的全部 reducers 循環執行一遍,而且這個函數要遵循(state, action) => newState 格式。還須要把每一個 reducer 的 initState 合併成一個 rootState。
實現以下:
export function combineReducers(reducers) { // reducerKeys = ['counter', 'info'] const reducerKeys = Object.keys(reducers) // 返回合併後的新的reducer函數 return function combination(state = {}, action) { // 生成的新的state const nextState = {} // 遍歷執行全部的reducers,整合成爲一個新的state for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] const reducer = reducers[key] // 以前的 key 的 state const previousStateForKey = state[key] // 執行 分 reducer,得到新的state const nextStateForKey = reducer(previousStateForKey, action) nextState[key] = nextStateForKey } return nextState } }
在大型 Web 應用程序中,一般須要將應用程序代碼拆分爲多個能夠按需加載的 JS 包。 這種稱爲「代碼分割」的策略經過減少初次加載時的 JS 的包的大小,來提升應用程序的性能。
reducer 拆分後,和組件是一一對應的。咱們就但願在作按需加載的時候,reducer 也能夠跟着組件在必要的時候再加載,而後用新的 reducer 替換老的 reducer。但實際上只有一個 root reducer 函數, 若是要實現的話就能夠用 replaceReducer 這個函數,實現以下:
const createStore = function (reducer, initState) { // ... const replaceReducer = (nextReducer) => { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } reducer = nextReducer // 刷新一遍 state 的值,新來的 reducer 把本身的默認狀態放到 state 樹上去 dispatch({ type: Symbol() }) } // ... return { // ... replaceReducer } }
使用以下:
const reducer = combineReducers({ counter: counterReducer }); const store = createStore(reducer); /*生成新的reducer*/ const nextReducer = combineReducers({ counter: counterReducer, info: infoReducer }); /*replaceReducer*/ store.replaceReducer(nextReducer);
bindActionCreators 通常比較少用到,在react-redux的 connect 函數實現會用到
會使用到 bindActionCreators 的場景是當你須要把 action creator 往下傳到一個組件上,卻不想讓這個組件覺察到 Redux 的存在,並且不但願把 dispatch 或 Redux store 傳給它。
咱們經過普通的方式來 隱藏 dispatch 和 actionCreator 試試
const reducer = combineReducers({ counter: counterReducer, info: infoReducer }); const store = createStore(reducer); // 返回 action 的函數就叫 actionCreator function increment() { return { type: 'INCREMENT' } } function getName() { return { type: 'FULL_NAME', } } const actions = { increment: function () { return store.dispatch(increment.apply(this, arguments)) }, getName: function () { return store.dispatch(getName.apply(this, arguments)) } } // 其餘地方在實現自增的時候,根本不知道 dispatch,actionCreator等細節 actions.increment(); // 自增 actions.getName(); // 得到全名
把actions生成時候的公共代碼提取出來:
const actions = bindActionCreators({ increment, getName }, store.dispatch)
bindActionCreators 的實現以下:
// 返回包裹 dispatch 的函數, 將 actionCreator 轉化成 dispatch 形式 // eg. { addNumAction } => (...args) => dispatch(addNumAction(args)) export function bindActionCreator(actionCreator, dispatch) { return function (...args) { return dispatch(actionCreator.apply(this, args)) } } export function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) } // actionCreators 必須是 function 或者 object if (typeof actionCreators !== 'object' || actionCreators === null) { throw new Error() } const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }
可能你們看到這裏有點懵逼,讓咱們來回憶下 react-redux 中 connect 函數的用法,
好比有這樣一個 actionCreators
// actionCreators.js function addNumAction() { return { type: 'ADD_NUM' } } // Demo.js:在須要用到 store 數據的組件,如 Demo 組件底部咱們用 connect 函數鏈接,以下: import { addNumAction } from './actionCreators' const mapDispatchToProps = (dispatch) => ({ addNum() { dispatch(addNumAction()) } }) export default connect(mapStateToProps, mapDispatchToProps)(Demo)
而後經過頁面的按鈕來出發 action 爲 ADD_NUM 對應事件
<button onClick={this.props.addNum}>增長1</button>
但除了上面的用法,mapDispatchToProps 也能夠這樣用,直接傳入一個對象,都沒有 dispatch 方法
export default connect(mapStateToProps, { addNumAction })(Demo)
而後只需觸發 addNumAction 就能實現和上面同樣的效果。
爲何能夠不傳,當你傳入對象的時候, connect 函數會判斷,大體代碼以下:
let dispatchToProps if (typeof mapDispatchToProps === 'function') { dispatchToProps = mapDispatchToProps(store.dispatch) } else { // 傳遞了一個 actionCreator 對象過來 dispatchToProps = bindActionCreators(mapDispatchToProps, store.dispatch) }
這裏就使用了 bindActionCreators 函數,它就是把你傳入的 actionCreator 再包一層 dispatch方法,即
{ addNumAction } => (...args) => dispatch(addNumAction(args))
Redux 實現講到這裏就結束了,把原理搞懂了確實對 Redux 的理解加深了好多,以後會繼續寫相關插件的實現,如 react-redux 等。
參考資料: