上次咱们已经写了 实现一个迷你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 等。
参考资料: