团队的项目中用了很久的 react 和 redux,一值想弄懂 redux 数据存储背后的逻辑,前段时间研究了下官方源码分享出来。这篇文章试图通过形象化的场景模拟来解读 redux 背后的设计,这种方式可能印象会更加深刻些。
redux的核心功能
redux的核心功能主要包括下面几个
- store: 可以简单描述成一个控制中心,可以获取 state,也可以改变 state,但是改变 state 需通过间接的方式
- reducer: 将 state 和其中变量的最新数据做一次合并返回 state
- action: 管理着改变 state 某些数据的行为,操作是同步的
- asyncAction: 管理着改变 state 某些数据的行为,操作是异步的,比如请求接口
- listener: 观察者模式下,由 store 控制着 listener,当有变更消息来时就触发 listener 执行更新
项目也常用到中间件比如 thunk
- thunk: 可以嵌套触发 action
场景转换
如果看不大懂上面的概念,没关系,或许看过下面的场景后会有一些认识的转变
我试图将上面的功能做一下场景的转变,场景是到店里吃饭,是生活中经常发生在你我身上的事情。如下我对功能做了标记
- store: 顾客
- thunk: 服务员
- action: 拿纸巾、加餐具、舔茶水、摆菜品
- asyncAction: 去后厨取菜品
- reducer: 餐桌(包括菜品区和碗碟区)
- listener: 算总价
顾客是整个就餐过程的核心人物,他可以吩咐服务员做各种事情,包括拿纸巾、上餐具、加茶水、上菜品等事情,我们假设,服务员每次拿完东西回来放在餐桌上后,顾客看到了会自发去算一遍桌上菜品(包括碗碟区)的总价。现在就开始点餐流程
两位顾客来到了名叫沫沫的餐厅找了一个桌位坐了下来,顾客看到餐桌上有一份纸巾和一套餐具,瞬间自发地在心里算了一笔帐。然后开始了点菜,他们点了清蒸鱼片、酸菜鱼和剁椒鱼头三道菜,于是服务员吩咐厨房开始制作,需要至少5分钟后才能做出菜品。
坐下来一小会儿,顾客A发现少了一份餐具,于是叫服务员拿份餐具,服务员取来餐具很有礼貌地放在了餐桌的碗碟区。这激发了顾客在心里算起了价格。
又过了一会儿,顾客B有点渴了,叫来服务员端两杯茶水,服务员取了两杯茶这次依然很有礼貌地放在了餐桌的碗碟区,这再次激发了顾客心里算起了价格,这就有点搞笑了,茶水是免费的,哈哈,不过这总归是条件反射嘛。
5分钟过去了,他们的三道菜品端上来了,服务员很自然地放在了餐桌的菜品区,这下顾客要好好算了,很得意地,顾客算好了。
没过多久,菜吃得差不多了,顾客A还没吃饱,于是吩咐服务员再点最后一道菜烤鱼,顺便再添加些茶水,这道菜后厨需要至少2分钟才能做出来,服务员吩咐了厨房并端来了茶水壶,给顾客加了两杯茶水。
2分钟过后,烤鱼来了,服务员摆放到了餐桌菜品区,顾客这次算了最后的总价,算完后两人开始吃起来了。
就餐的过程中两位顾客相聊甚欢,吃得差不多了准备起身结账时,发现餐桌上有一份纸巾,顾客并不需要这个纸巾,于是叫服务员拿掉了这个纸巾,并再次算起了总价,这次真的是最后的总价了。
最后顾客到前台完成了结账,满心欢喜地回家了。
看到这儿,如果能看懂,其实对 redux 的底部设计了解得差不多了。
为了将这个故事反应到 redux 的设计上,我准备将关键信息和 redux 的关键功能对应上如下。
我们大概已经知道 redux 面对一个 action 的内部处理流程了,之所以需要这么规范化复杂化的流程设计,想必就是各司其职,防止越位,最后防止 state 被恶意篡改。
开启源码解读
这篇文章就这么结束了吗,想的美,接下来就是重头戏。
对 redux 的设计有了一些认识后,我们结合它的核心源码来探究它是怎么完成上面的设计的,源码使用的是 gh-pages 版本,只提取其中的核心部分,下面是 redux 的 github 源码目录,包括 creatStore、compose、combineReducers、applyMiddleware,我们暂且略过 bindActionCreators,并引入 thunk 一起看看他们是怎么结合的。
场景转为示例
首先抛出一个具体的示例,仿真一开始的故事情节编写。为了模拟多个 reducer 模块,我们将加餐具、舔茶水、退纸巾的操作看成一个模块,针对菜品的操作看成另一个模块。
这是 reducer1,存储着餐具、茶水、纸巾的初始化数据,以及更改这些数据的操作
1 | // reducer1.js |
这是 action1,对应着 reducer1,标识着改变 state 某些数据的操作行为,包括加餐具、舔茶水、退纸巾,当然也可以对数据做一些处理后转为最终态
1 | // action1.js |
这是reducer2,存储着菜品的初始数据
1 | // reducer2.js |
这是 action2,对应着 reducer2,包含针对菜品的操作
1 | // action2.js |
这是 asyncAction1,在这里的 action 都包含有异步操作,通常是请求接口数据,包括去后厨取菜品
1 | // asyncAction1.js |
这里是生成控制中心 store 的地方,也就是顾客
1 | // store.js |
至此,我们完成了一个 redux 的基本结构,包括两个 reducer 及对应的 action,同时还有一个 asyncAction 以模拟包含异步的操作(取菜品对应请求接口这种类似的操作),最后在 store.js 中做两个 reducer 的合并,同时引入中间件 thunk 做 store 初始化。之后就是页面引入这个 store 就可以做相关的业务逻辑了,这块的会涉及到 react-redux 相关,到时会单独来讲解,在这里咱们只关注 redux 的内部实现。
createStore
接下来就是,怎么去看redux这个源码,我们顺着store的初始化来看,因为这是页面逻辑唯一引入redux的地方。可以看到有几个函数我们需要关注
- combineReducers
- createStore
- composeEnhancers
- applyMiddleware
- thunk
其实这基本是今天要说的全部内容了。
首先看 createStore,这是 store 初始化的地方,我们剔除源码中的异常等逻辑,只提取核心逻辑来看。
1 | // 来源 redux 中的 createStore.js,做了部分修改,剔除了replaceReducer和[$$observable] |
结合 store.js 中的 createStore 来看,不考虑插件情况 composeEnhancers 即为 compose
1 | const store = createStore(rootReducer, compose(applyMiddleware(thunk))); |
其中
- rootReducer 对应 reducer
- compose(applyMiddleware(thunk)) 对应 preloadedState
- enhancer 是 undefined
createStore 最后返回的是
1 | enhancer(createStore)(reducer, preloadedState) |
即执行下方,其中 preloadedState 为 undefined,并且 createStore 被作为参数传入
1 | (compose(applyMiddleware(thunk))(createStore))(reducer, preloadedState) |
compose
看起来是相当复杂的,我们一步步来看,首先是看下 compose 它的作用
1 | // 来源 redux 中的 compose.js |
代码很简洁,可以看出主要用于多个函数的合并操作,按照 funcs 的顺序从右向左做合并,其中 b 接受到参数 args 后运行的结果作为 a 的参数,a 的运行参数作为 a 左边函数的参数,这样将 funcs 遍历一遍,用一个栗子来看看
1 | function add (a) { |
这时,10 相当于 args 被传入最右边的 add(3),即执行 add(3)(10) 后结果是 13,13 作为 add(2) 的参数即执行 add(2)(13) 结果是 15,最后执行 add(1)(15),最后的结果16
回过头来再来看看之前的案例
1 | (compose(applyMiddleware(thunk))(createStore))(reducer, preloadedState) |
applyMiddleware
接着,我们看下 applyMiddleware 的原理,其中 middlewares 对应 thunk,当然也可以传多个中间件,这里我们只使用了一个,args 对应着 reducer 和 preloadedState,终于对上号了
1 | export default function applyMiddleware(...middlewares) { |
最后的运行结果即是我们最初通过 createStore 初始化后的 store 对象,可以看到,其中的 dispatch 被重写了,是被 middleware 即我们传入的 thunk 重写了,因为 middlewares 长度为1,因此 chain 就只有一个元素, compose 函数传入的 chain 即是
1 | middleware(middlewareAPI) |
thunk
做了 thunk(middlewareAPI)(store.dispatch) 的执行后,得到了新的 dispatch,是时候来看看 thunk 了
1 | function createThunkMiddleware(extraArgument) { |
也是非常简洁,我们改写下,其中参数中的 dispatch 和 getState 分别对应 middlewareAPI 中的 dispatch 和 getState,next 是 store.dispatch,action 即是业务逻辑中通过 dispatch(action) 提交的 action,这里返回的是函数,当业务逻辑中通过 dispatch(action) 时,action 可以是对象,也可以是函数;如果是函数,默认会把 dispatch, getState, extraArgument 传入,即可以在一个 action 中嵌套 action,如案例中的 getDishs 即是这样的一个 action,而 deleteNapkin 是一个对象 action,其他的如 placeDishsAction 是没有嵌套 action 的 action,当然它也是一个函数
1 | export default function({ dispatch, getState }) { |
combineReducers
最后,我们来看下遗漏的点就是把多个 reducer 合并进行统一管理
1 | // redux 中的 combineReducers.js |
在 combineReducers 中,其中 reducers 就是 reducer1 和 reducer2,通过合并后存储哈希表 finalReducers 管理如下,key 为 reducer 名,func 是对应 reducer 的处理逻辑,当通过 dispatch(action) 时,会执行 combination 逻辑,遍历并执行所有的 reducer 获取最新的 state,并和之前的 state 对比以确认返回的 state
1 | const finalReducers = { |
不过需要注意的一点是,在 createStore 的最后会执行 dispatch({ type: ActionTypes.INIT }),这一步是初始化 state 操作,因为一开始传递的 preloadedState 是 undefined,即 createStore 中的 currentState 是 undefined,当通过 getState 获取 state 时是空的,因此需要做一次初始化操作,初始化时执行 combination 逻辑,依然是遍历每个 reducer,每个 reducer 的第一个参数 state 默认是初始化的参数 initialState,最后在 nextState 存储的将是如下结构,即通过 getState 获取的也将是这个结构
1 | const nextState = { |
总结
看到这里,对 redux 的内部实现是不是有了一些认识,如果还是有些模糊,不妨结合源码和示例在脑海里多运行几遍,希望能对你有所帮助!