THE LAST TIME
The last time, I have learned
【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是给自己的查缺补漏和技术分享。
笔者文章集合详见:
TLT往期
前言
范式概念是库恩范式理论的核心,而范式从本质上讲是一种理论体系。库恩指出:按既定的用法,范式就是一种公认的模型或模式。
而学习 Redux,也并非它的源码有多么复杂,而是他状态管理的思想,着实值得我们学习。
讲真,标题真的是不好取,因为本文是我写的 redux 的下一篇。两篇凑到一起,才是完整的 Redux。
上篇:从 Redux 设计理念到源码分析
本文续上篇,接着看 combineReducers、applyMiddleware和 compose 的设计与源码实现
至于手写,其实也是非常简单,说白了,去掉源码中严谨的校验,就是市面上手写了。当然,本文,我也尽量以手写演进的形式,去展开剩下几个 api 的写法介绍。
combineReducers
从上一篇中我们知道,newState 是在 dispatch 的函数中,通过 currentReducer(currentState,action)拿到的。所以 state 的最终组织的样子,完全的依赖于我们传入的 reducer。而随着应用的不断扩大,state 愈发复杂,redux 就想到了分而治之(我寄几想的词儿)。虽然最终还是一个根,但是每一个枝放到不同的文件 or func 中处理,然后再来组织合并。(模块化有么有)
combineReducers 并不是 redux 的核心,或者说这是一个辅助函数而已。但是我个人还是喜欢这个功能的。它的作用就是把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。
进化过程
比如我们现在需要管理这么一个"庞大"的 state:

letstate={name:'Nealyang',baseInfo:{age:'25',gender:'man'},other:{github:'https://github.com/Nealyang',WeChatOfficialAccount:'全栈前端精选'}}因为太庞大了,写到一个 reducer 里面去维护太难了。所以我拆分成三个 reducer。
functionnameReducer(state,action){switch(action.type){case"UPDATE":
returnaction.name;default:
returnstate;}}functionbaseInfoReducer(state,action){switch(action.type){case"UPDATE_AGE":
return{
...state,age: action.age,};case"UPDATE_GENDER":
return{
...state,age: action.gender,};default:
returnstate;}}functionotherReducer(state,action){...}为了他这个组成一个我们上文看到的 reducer,我们需要搞个这个函数
constreducer=combineReducers({name:nameReducer,baseInfo:baseInfoReducer,other:otherReducer})所以,我们现在自己写一个 combineReducers
functioncombineReducers(reducers){constreducerKeys=Object.keys(reducers);returnfunction(state={},action){constnextState={};for(leti=0,keyLen=reducerKeys.length;i<keyLen;i++){// 拿出 reducers 的 key,也就是 name、baseInfo、otherconstkey=reducerKeys[i];// 拿出如上的对应的 reducer: nameReducer、baseInfoReducer、otherReducerconstreducer=reducers[key];// 去除需要传递给对应 reducer 的初始 stateconstpreStateKey=state[key];// 拿到对应 reducer 处理后的 stateconstnextStateKey=reducer(preStateKey,action);// 赋值给新 state 的对应的 key 下面nextState[key]=nextStateKey;}returnnextState;}}基本如上,我们就完事了。
关于 reducer 更多的组合、拆分、使用的,可以参照我 github 开源的前后端博客的 Demo:React-Express-Blog-Demo

源码
exporttypeReducer<S=any,AextendsAction=AnyAction>=(state: S|undefined,action: A)=>SexporttypeReducersMapObject<S=any,AextendsAction=Action>={[KinkeyofS]: Reducer<S[K], A>}定义了一个需要传递给 combineReducers 函数的参数类型。也就是我们上面的
{name:nameReducer,baseInfo:baseInfoReducer,other:otherReducer}其实就是变了一个 state 的 key,然后 key 对应的值是这个 Reducer,这个 Reducer 的 state 是前面取出这个 key 的state 下的值。
exportdefaultfunctioncombineReducers(reducers: ReducersMapObject){//获取所有的 key,也就是未来 state 的 key,同时也是此时 reducer 对应的 keyconstreducerKeys=Object.keys(reducers)// 过滤一遍 reducers 对应的 reducer 确保 kv 格式么有什么毛病constfinalReducers: ReducersMapObject={}for(leti=0;i<reducerKeys.length;i++){constkey=reducerKeys[i]if(process.env.NODE_ENV!=='production'){if(typeofreducers[key]==='undefined'){warning(`No reducer provided for key "${key}"`)}}if(typeofreducers[key]==='function'){finalReducers[key]=reducers[key]}}// 再次拿到确切的 keyArrayconstfinalReducerKeys=Object.keys(finalReducers)// This is used to make sure we don't warn about the same// keys multiple times.
let unexpectedKeyCache: {[key: string]: true}if(process.env.NODE_ENV!=='production'){unexpectedKeyCache={}}
let shapeAssertionError: Errortry{// 校验自定义的 reducer 一些基本的写法assertReducerShape(finalReducers)}catch(e){shapeAssertionError=e}// 重点是这个函数returnfunctioncombination(state: StateFromReducersMapObject<typeofreducers>={},action: AnyAction){if(shapeAssertionError){throwshapeAssertionError}if(process.env.NODE_ENV!=='production'){constwarningMessage=getUnexpectedStateShapeWarningMessage(state,finalReducers,action,unexpectedKeyCache)if(warningMessage){warning(warningMessage)}}lethasChanged=falseconstnextState: StateFromReducersMapObject<typeofreducers> = {}
for (let i = 0; i <finalReducerKeys.length;i++){constkey=finalReducerKeys[i]constreducer=finalReducers[key]constpreviousStateForKey=state[key]constnextStateForKey=reducer(previousStateForKey,action)// 上面的部分都是我们之前手写内容,nextStateForKey 是返回的一个newState,判断不能为 undefinedif(typeofnextStateForKey==='undefined'){consterrorMessage=getUndefinedStateErrorMessage(key,action)thrownewError(errorMessage)}nextState[key]=nextStateForKey// 判断是否改变,这里其实我还是很疑惑// 理论上,reducer 后的 newState 无论怎么样,都不会等于 preState 的hasChanged=hasChanged||nextStateForKey!==previousStateForKey}hasChanged=hasChanged||finalReducerKeys.length!==Object.keys(state).lengthreturnhasChanged ? nextState : state}}combineReducers 代码其实非常简单,核心代码也就是我们上面缩写的那样。但是我是真的喜欢这个功能。

applyMiddleware
说 applyMiddleware 这个方法,其实不得不说,redux 中的 Middleware。中间件的概念不是 redux 独有的。Express、Koa等框架,也都有这个概念。只是为解决不同的问题而存在罢了。
Redux 的 Middleware 说白了就是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能! 一般我们常用的可以记录日志、错误采集、异步调用等。
其实关于Redux 的 Middleware, 我觉得中文文档说的就已经非常棒了,这里我简单介绍下。感兴趣的可以查看详细的介绍:Redux 中文文档
Middleware 演化过程
记录日志的功能增强
- 需求:在每次修改
state 的时候,记录下来 修改前的 state ,为什么修改了,以及修改后的 state。 - Action:每次修改都是
dispatch 发起的,所以这里我只要在 dispatch 加一层处理就一劳永逸了。
conststore=createStore(reducer);constnext=store.dispatch;/*重写了store.dispatch*/store.dispatch=(action)=>{console.log('this state',store.getState());console.log('action',action);next(action);console.log('next state',store.getState());}如上,在我们每一次修改 dispatch 的时候都可以记录下来日志。因为我们是重写了 dispatch 不是。
增加个错误监控的增强
conststore=createStore(reducer);constnext=store.dispatch;store.dispatch=(action)=>{try{next(action);}catch(err){console.error('错误报告: ',err)}}所以如上,我们也完成了这个需求。
但是,回头看看,这两个需求如何才能够同时实现,并且能够很好地解耦呢?
想一想,既然我们是增强 dispatch。那么是不是我们可以将 dispatch 作为形参传入到我们增强函数。
多文件增强
constexceptionMiddleware=(next)=>(action)=>{try{/*loggerMiddleware(action);*/next(action);}catch(err){console.error('错误报告: ',err)}}/*loggerMiddleware 变成参数传进去*/store.dispatch=exceptionMiddleware(loggerMiddleware);// 这里额 next 就是最纯的 store.dispatch 了constloggerMiddleware=(next)=>(action)=>{console.log('this state',store.getState());console.log('action',action);next(action);console.log('next state',store.getState());}所以最终使用的时候就如下了
conststore=createStore(reducer);constnext=store.dispatch;constloggerMiddleware=(next)=>(action)=>{console.log('this state',store.getState());console.log('action',action);next(action);console.log('next state',store.getState());}constexceptionMiddleware=(next)=>(action)=>{try{next(action);}catch(err){console.error('错误报告: ',err)}}store.dispatch=exceptionMiddleware(loggerMiddleware(next));但是如上的代码,我们又不能将 Middleware 独立到文件里面去,因为依赖外部的 store。所以我们再把 store 传入进去!
conststore=createStore(reducer);constnext=store.dispatch;constloggerMiddleware=(store)=>(next)=>(action)=>{console.log('this state',store.getState());console.log('action',action);next(action);console.log('next state',store.getState());}constexceptionMiddleware=(store)=>(next)=>(action)=>{try{next(action);}catch(err){console.error('错误报告: ',err)}}constlogger=loggerMiddleware(store);constexception=exceptionMiddleware(store);store.dispatch=exception(logger(next));以上其实就是我们写的一个 Middleware,理论上,这么写已经可以满足了。但是!是不是有点不美观呢?且阅读起来非常的不直观呢?
如果我需要在增加个中间件,调用就成为了
store.dispatch=exception(time(logger(action(xxxMid(next)))))
这也就是 applyMiddleware 的作用所在了。
我们只需要知道有多少个中间件,然后在内部顺序调用就可以了不是
constnewCreateStore=applyMiddleware(exceptionMiddleware,timeMiddleware,loggerMiddleware)(createStore);conststore=newCreateStore(reducer)
手写 applyMiddleware
constapplyMiddleware=function(...middlewares){// 重写createStore 方法,其实就是返回一个带有增强版(应用了 Middleware )的 dispatch 的 storereturnfunctionrewriteCreateStoreFunc(oldCreateStore){// 返回一个 createStore 供外部调用returnfunctionnewCreateStore(reducer,initState){// 把原版的 store 先取出来conststore=oldCreateStore(reducer,initState);// const chain = [exception, time, logger] 注意这里已经传给 Middleware store 了,有了第一次调用constchain=middlewares.map(middleware=>middleware(store));// 取出原先的 dispatchletdispatch=store.dispatch;// 中间件调用时←,但是数组是→。所以 reverse。然后在传入 dispatch 进行第二次调用。最后一个就是 dispatch func 了(回忆 Middleware 是不是三个括号~~~)chain.reverse().map(middleware=>{dispatch=middleware(dispatch);});store.dispatch=dispatch;returnstore;}}}解释全在代码上了
其实源码里面也是这么个逻辑,但是源码实现更加的优雅。他利用了函数式编程的 compose 方法。在看 applyMiddleware 的源码之前呢,先介绍下 compose 的方法吧。
compose
其实 compose 函数做的事就是把 var a = fn1(fn2(fn3(fn4(x)))) 这种嵌套的调用方式改成 var a = compose(fn1,fn2,fn3,fn4)(x) 的方式调用。
compose的运行结果是一个函数,调用这个函数所传递的参数将会作为compose最后一个参数的参数,从而像'洋葱圈'似的,由内向外,逐步调用。
exportdefaultfunctioncompose(...funcs: Function[]){if(funcs.length===0){// infer the argument type so it is usable in inference down the linereturn<T>(arg: T) =>arg}if(funcs.length===1){returnfuncs[0]}returnfuncs.reduce((a,b)=>(...args: any)=>a(b(...args)))}哦豁!有点蒙有么有~ 函数式编程就是烧脑🤯且直接。所以爱的人非常爱。
compose是函数式编程中常用的一种组合函数的方式。
方法很简单,传入的形参是 func[],如果只有一个,那么直接返回调用结果。如果是多个,则funcs.reduce((a, b) => (...args: any) => a(b(...args))).
我们直接啃最后一行吧
import{componse}from'redux'functionadd1(str){return1+str;}functionadd2(str){return2+str;}functionadd3(a,b){returna+b;}letstr=compose(add1,add2,add3)('x','y')console.log(str)//输出结果 '12xy'
dispatch = compose<typeof dispatch>(...chain)(store.dispatch) applyMiddleware 的源码最后一行是这个。其实即使我们上面手写的 reverse 部分。
reduce 是 es5 的数组方法了,对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。函数签名为:arr.reduce(callback[, initialValue])
所以如若我们这么看:
[func1,func2,func3].reduce(function(a,b){returnfunction(...args){returna(b(...args))}})所以其实就非常好理解了,每一次 reduce 的时候,callback 的a,就是一个a(b(...args))的 function,当然,第一次是 a 是 func1。后面就是无限的叠罗汉了。最终拿到的是一个 func1(func2(func3(...args)))的 function。
总结
所以回头看看,redux 其实就这么些东西,第一篇算是 redux 的核心,关于状态管理的思想和方式。第二篇可以理解为 redux 的自带的一些小生态。全部的代码不过两三百行。但是这种状态管理的范式,还是非常指的我们再去思考、借鉴和学习的。
学习交流
- 关注公众号【全栈前端精选】,每日获取好文推荐
- 添加微信号:is_Nealyang(备注来源) ,入群交流
| 公众号【全栈前端精选】 | 个人微信【is_Nealyang】 |
|---|
 |  |
THE LAST TIME
【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是给自己的查缺补漏和技术分享。
笔者文章集合详见:
TLT往期
前言
范式概念是库恩范式理论的核心,而范式从本质上讲是一种理论体系。库恩指出:按既定的用法,范式就是一种公认的模型或模式。
而学习
Redux,也并非它的源码有多么复杂,而是他状态管理的思想,着实值得我们学习。讲真,标题真的是不好取,因为本文是我写的
redux的下一篇。两篇凑到一起,才是完整的Redux。上篇:从 Redux 设计理念到源码分析
本文续上篇,接着看
combineReducers、applyMiddleware和compose的设计与源码实现至于手写,其实也是非常简单,说白了,去掉源码中严谨的校验,就是市面上手写了。当然,本文,我也尽量以手写演进的形式,去展开剩下几个
api的写法介绍。combineReducers
从上一篇中我们知道,
newState是在dispatch的函数中,通过currentReducer(currentState,action)拿到的。所以state的最终组织的样子,完全的依赖于我们传入的reducer。而随着应用的不断扩大,state愈发复杂,redux就想到了分而治之(我寄几想的词儿)。虽然最终还是一个根,但是每一个枝放到不同的文件 orfunc中处理,然后再来组织合并。(模块化有么有)combineReducers并不是redux的核心,或者说这是一个辅助函数而已。但是我个人还是喜欢这个功能的。它的作用就是把一个由多个不同reducer函数作为value的object,合并成一个最终的reducer函数。进化过程
比如我们现在需要管理这么一个"庞大"的
state:因为太庞大了,写到一个
reducer里面去维护太难了。所以我拆分成三个reducer。为了他这个组成一个我们上文看到的
reducer,我们需要搞个这个函数所以,我们现在自己写一个
combineReducers基本如上,我们就完事了。
关于
reducer更多的组合、拆分、使用的,可以参照我github开源的前后端博客的 Demo:React-Express-Blog-Demo源码
定义了一个需要传递给
combineReducers函数的参数类型。也就是我们上面的其实就是变了一个
state的key,然后key对应的值是这个Reducer,这个Reducer的state是前面取出这个key的state下的值。combineReducers代码其实非常简单,核心代码也就是我们上面缩写的那样。但是我是真的喜欢这个功能。applyMiddleware
说
applyMiddleware这个方法,其实不得不说,redux中的Middleware。中间件的概念不是redux独有的。Express、Koa等框架,也都有这个概念。只是为解决不同的问题而存在罢了。Redux的Middleware说白了就是对dispatch的扩展,或者说重写,增强dispatch的功能! 一般我们常用的可以记录日志、错误采集、异步调用等。其实关于
Redux的Middleware, 我觉得中文文档说的就已经非常棒了,这里我简单介绍下。感兴趣的可以查看详细的介绍:Redux 中文文档Middleware 演化过程
记录日志的功能增强
state的时候,记录下来 修改前的state,为什么修改了,以及修改后的state。dispatch发起的,所以这里我只要在dispatch加一层处理就一劳永逸了。如上,在我们每一次修改
dispatch的时候都可以记录下来日志。因为我们是重写了dispatch不是。增加个错误监控的增强
所以如上,我们也完成了这个需求。
但是,回头看看,这两个需求如何才能够同时实现,并且能够很好地解耦呢?
想一想,既然我们是增强 dispatch。那么是不是我们可以将 dispatch 作为形参传入到我们增强函数。
多文件增强
所以最终使用的时候就如下了
但是如上的代码,我们又不能将 Middleware 独立到文件里面去,因为依赖外部的
store。所以我们再把store传入进去!以上其实就是我们写的一个
Middleware,理论上,这么写已经可以满足了。但是!是不是有点不美观呢?且阅读起来非常的不直观呢?如果我需要在增加个中间件,调用就成为了
这也就是
applyMiddleware的作用所在了。我们只需要知道有多少个中间件,然后在内部顺序调用就可以了不是
手写 applyMiddleware
其实源码里面也是这么个逻辑,但是源码实现更加的优雅。他利用了函数式编程的
compose方法。在看applyMiddleware的源码之前呢,先介绍下 compose 的方法吧。compose
其实
compose函数做的事就是把var a = fn1(fn2(fn3(fn4(x))))这种嵌套的调用方式改成var a = compose(fn1,fn2,fn3,fn4)(x)的方式调用。compose的运行结果是一个函数,调用这个函数所传递的参数将会作为compose最后一个参数的参数,从而像'洋葱圈'似的,由内向外,逐步调用。哦豁!有点蒙有么有~ 函数式编程就是烧脑🤯且直接。所以爱的人非常爱。
compose是函数式编程中常用的一种组合函数的方式。方法很简单,传入的形参是 func[],如果只有一个,那么直接返回调用结果。如果是多个,则
funcs.reduce((a, b) => (...args: any) => a(b(...args))).我们直接啃最后一行吧
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)applyMiddleware 的源码最后一行是这个。其实即使我们上面手写的 reverse 部分。reduce 是 es5 的数组方法了,对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。函数签名为:
arr.reduce(callback[, initialValue])所以如若我们这么看:
所以其实就非常好理解了,每一次
reduce的时候,callback的a,就是一个a(b(...args))的function,当然,第一次是a是func1。后面就是无限的叠罗汉了。最终拿到的是一个func1(func2(func3(...args)))的function。总结
所以回头看看,
redux其实就这么些东西,第一篇算是redux的核心,关于状态管理的思想和方式。第二篇可以理解为redux的自带的一些小生态。全部的代码不过两三百行。但是这种状态管理的范式,还是非常指的我们再去思考、借鉴和学习的。学习交流