上篇写了实现一个迷你 Redux(基础版),本篇将继续完善 Redux
阅读本文前先看:实现一个迷你 Redux(基础版) 
中间件 Redux 有个 API 是 applyMiddleware, 专门用来使用中间件的,首先我们得知道,它用来干嘛的。
为什么会需要中间件? 假设我们现在需要记录每次的 dispatch 前后 state 的记录, 那要怎么做呢?于是,简单粗暴的在第一个 dispatch 方法前后加代码
1 2 3 4 console .log('prev state' , store.getState())console .log(action)store.dispatch({ type : 'INCREMENT'  }) console .log('next state' , store.getState())
这部分运行结果:
1 2 3 4 prev state {value : 10 } {type : "INCREMENT" } 当前数字为:11  next state {value : 11 } 
但加完发现情况不对,页面有多个 dispatch 的话,要这样写很多次,会产生大量重复代码。突然,又要加需求了,需要记录每次出错的原因,单独的功能要求如下:
1 2 3 4 5 try  {  store.dispatch(action) } catch  (err) {   console .error('错误信息: ' , err) } 
然后两个需求都要,那就凑合两个,但叠一起看更乱了。
中间件的概念 显然,我们不能通过这种方式来做。比较理想的方案是 Redux 本身提供一个功能入口,让我们可以在外面添加功能进去,这样代码就不会复杂。
但如果给我们现有实现的 Redux 添加功能,在哪个环节添加比较合适呢?
Reducer: 纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。 View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。 Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。 我们发现,以上需求都是和 dispatch 相关,只有发送 action 的这个步骤,即 store.dispatch() 方法,可以添加功能。比如添加日志功能,我们只要把日志放进 dispatch 函数里,不就好了吗,我们只需要改造 dispatch 函数,把 dispatch 进行一层封装。
1 2 3 4 5 6 7 8 9 10 11 12 const  store = createStore(counter)const  next = store.dispatchstore.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 函数就混乱到无法维护了,故需要扩展性强的多中间件合作模式。
我们把 loggerMiddleware 提取出来 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const  store = createStore(counter)const  next = store.dispatchconst  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)   } } 
把 exceptionMiddleware 提取出来 1 2 3 4 5 6 7 8 9 const  exceptionMiddleware = action  =>  {  try  {     loggerMiddleware(action)   } catch  (err) {     console .error('错误信息: ' , err)   } } store.dispatch = exceptionMiddleware 
现在代码有个问题,就是 exceptionMiddleware 中间件写死 loggerMiddleware,但以后又万一不要记录功能呢,所以我们需要让 next(action) 变成动态的,即换哪个中间件都可以 1 2 3 4 5 6 7 8 const  exceptionMiddleware = next  =>  action  =>  {  try  {          next(action)   } catch  (err) {     console .error('错误信息: ' , err)   } } 
这个写法可能刚开始看不太适应,实际就是函数里面,返回一个函数,即等效于
1 2 3 4 5 6 7 8 9 10 const  exceptionMiddleware = function  (next )  {  return  function  (action )  {     try  {              next(action)     } catch  (err) {       console .error('错误信息: ' , err)     }   } } 
传参数的时候即是 exceptionMiddleware(next)(action)
同理,我们让 loggerMiddleware 里面无法扩展别的中间件了!我们也把 next 写成动态的 1 2 3 4 5 6 const  loggerMiddleware = next  =>  action  =>  {  console .log('prev state' , store.getState())   console .log(action)   next(action)   console .log('next state' , store.getState()) } 
目前为止,整个中间件设计改造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const  store = createStore(counter)const  next = store.dispatchconst  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)) 
现在又有一个新问题,想想平时使用中间件是从外部引入的,那外部中间件里面怎么会有 store.getState() 这个方法,于是我们把 store 也给独立出去。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const  store = createStore(counter)const  next = store.dispatchconst  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)) 
如果又有一个新需求,需要在打印日志前输出当前时间戳,我们又需要构造一个中间件 1 2 3 4 5 6 7 8 9 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 来实现。 先来看看期望的用法:
1 2 3 4 5 const  newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore)const  store = newCreateStore(reducer)
实现 applyMiddleware 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export  const  applyMiddleware = function  (...middlewares )  {     return  function  rewriteCreateStoreFunc (oldCreateStore )  {          return  function  newCreateStore (reducer, preloadedState )  {              const  store = oldCreateStore(reducer, preloadedState)       let  dispatch = store.dispatch              const  middlewareAPI = {         getState: store.getState,         dispatch: action  =>  store.dispatch(action),       }                            const  chain = middlewares.map(middleware  =>  middleware(middlewareAPI))              chain.reverse().map(middleware  =>  {         dispatch = middleware(dispatch)       })              store.dispatch = dispatch       return  store     }   } } 
我们来看这一处代码:
1 2 3 chain.reverse().map(middleware  =>  {   dispatch = middleware(dispatch) }) 
要注意一点,中间件是顺序执行,但是 dispatch 却是反序生成的。所以在这步会把数组顺序给反序(比如 applyMiddleware(A, B, C),因为 A 在调用时需要知道 B 的 dispatch,B 在执行时需要知道 C 的 dispatch,那么需要先知道 C 的 dispatch。)
官方 Redux 源码,采用了 compose 函数,我们也试试这种方式来写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export  const  applyMiddleware = (...middlewares ) =>  {  return  createStore  =>  (...args ) =>  {          dispatch = compose(...chain)(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))) } 
我们再对代码精简:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 了,这怎么区分,上篇我们其实已经先告知了对中间件代码处理,但具体怎么推出的,我们继续看。
1 2 3 4 5 6 7 const  store = createStore(counter)const  rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)const  newCreateStore = rewriteCreateStoreFunc(createStore)const  store = newCreateStore(counter, preloadedState)
为了让用户用起来统一一些,我们可以很简单的使他们的使用方式一致,我们修改下 createStore 方法
1 2 3 4 5 6 7 8 const  createStore = (reducer, preloadedState, rewriteCreateStoreFunc ) =>  {     if  (rewriteCreateStoreFunc) {     const  newCreateStore = rewriteCreateStoreFunc(createStore)     return  newCreateStore(reducer, preloadedState)   }    } 
不过 Redux 源码 rewriteCreateStoreFunc 换了个名字,还加了判断,也就是我们上篇的代码:
1 2 3 4 5 6 if  (typeof  enhancer !== 'undefined' ) {  if  (typeof  enhancer !== 'function' ) {     throw  new  Error ('Expected the enhancer to be a function.' )   }   return  enhancer(createStore)(reducer, preloadedState) } 
所以中间件的用法为
1 const  store = createStore(counter,  applyMiddleware(logger))
combineReducers 如果我们做的项目很大,有大量 state,那么维护起来很麻烦。Redux 提供了 combineReducers 这个方法,作用是把多个 reducer 合并成一个 reducer, 每个 reducer 负责独立的模块。
我们用一个新例子来举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import  { createStore, applyMiddleware, combineReducers } from  'redux' const  initCounterState = {  value: 10 , } const  initInfoState = {  name: 'jacky' , } const  reducer = combineReducers({  counter: counterReducer,   info: infoReducer, }) 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()console .log(`一开始counter为:${init.counter.value} ,info为 ${init.info.name} ` )function  listener ( )  {  store.getState() } store.subscribe(listener)  store.dispatch({ type : 'INCREMENT'  }) store.dispatch({ type : 'INCREMENT'  }) store.dispatch({ type : 'DECREMENT'  }) store.dispatch({ type : 'FULL_NAME'  }) console .log(`执行完counter为:${store.getState().counter.value} ,info为${store.getState().info.name} ` )export  default  store
我们来尝试下如何实现这个 API,
首先要把一个函数里的所有 reducers 循环执行一遍,并且这个函数要遵循(state, action) => newState 格式。还需要把每个 reducer 的 initState 合并成一个 rootState。 实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export  function  combineReducers (reducers )  {     const  reducerKeys = Object .keys(reducers)      return  function  combination (state = {}, action )  {          const  nextState = {}          for  (let  i = 0 ; i < reducerKeys.length; i++) {       const  key = reducerKeys[i]       const  reducer = reducers[key]              const  previousStateForKey = state[key]              const  nextStateForKey = reducer(previousStateForKey, action)       nextState[key] = nextStateForKey     }     return  nextState   } } 
replaceReducer 在大型 Web 应用程序中,通常需要将应用程序代码拆分为多个可以按需加载的 JS 包。 这种称为“代码分割”的策略通过减小初次加载时的 JS 的包的大小,来提高应用程序的性能。
reducer 拆分后,和组件是一一对应的。我们就希望在做按需加载的时候,reducer 也可以跟着组件在必要的时候再加载,然后用新的 reducer 替换老的 reducer。但实际上只有一个 root reducer 函数, 如果要实现的话就可以用 replaceReducer 这个函数,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const  createStore = function  (reducer, initState )  {     const  replaceReducer = nextReducer  =>  {     if  (typeof  nextReducer !== 'function' ) {       throw  new  Error ('Expected the nextReducer to be a function.' )     }     reducer = nextReducer          dispatch({ type : Symbol () })   }      return  {          replaceReducer,   } } 
使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 const  reducer = combineReducers({  counter: counterReducer, }) const  store = createStore(reducer)const  nextReducer = combineReducers({  counter: counterReducer,   info: infoReducer, }) store.replaceReducer(nextReducer) 
bindActionCreators bindActionCreators 一般比较少用到,在 react-redux 的 connect 函数实现会用到
会使用到 bindActionCreators 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 dispatch 或 Redux store 传给它。
我们通过普通的方式来 隐藏 dispatch 和 actionCreator 试试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const  reducer = combineReducers({  counter: counterReducer,   info: infoReducer, }) const  store = createStore(reducer)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 ))   }, } actions.increment()  actions.getName()  
把 actions 生成时候的公共代码提取出来:
1 const  actions = bindActionCreators({ increment, getName }, store.dispatch)
bindActionCreators 的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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)   }      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
1 2 3 4 5 6 7 8 9 10 11 12 function  addNumAction ( )  {  return  { type : 'ADD_NUM'  } } import  { addNumAction } from  './actionCreators' const  mapDispatchToProps = dispatch  =>  ({  addNum() {     dispatch(addNumAction())   }, }) export  default  connect(mapStateToProps, mapDispatchToProps)(Demo)
然后通过页面的按钮来出发 action 为 ADD_NUM 对应事件
1 <button onClick={this .props.addNum}>增加1 </button>  
但除了上面的用法,mapDispatchToProps 也可以这样用,直接传入一个对象,都没有 dispatch 方法
1 export  default  connect(mapStateToProps, { addNumAction })(Demo)
然后只需触发 addNumAction 就能实现和上面一样的效果。
为什么可以不传,当你传入对象的时候, connect 函数会判断,大致代码如下:
1 2 3 4 5 6 7 8 let  dispatchToPropsif  (typeof  mapDispatchToProps === 'function' ) {  dispatchToProps = mapDispatchToProps(store.dispatch) } else  {      dispatchToProps = bindActionCreators(mapDispatchToProps, store.dispatch) } 
这里就使用了 bindActionCreators 函数,它就是把你传入的 actionCreator 再包一层 dispatch 方法,即
1 { addNumAction }  =>  (...args ) =>  dispatch(addNumAction(args)) 
总结 Redux 实现讲到这里就结束了,把原理搞懂了确实对 Redux 的理解加深了好多,之后会继续写相关插件的实现,如 react-redux 等。
参考资料:
完全理解 redux(从零实现一个 redux)