“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.” —Martin Golding
前言
Redux 官方文档已经详细介绍了Redux的动机、核心概念以及背后的设计思想,这里就简单总结下不再赘述:
- Redux是一个可预测的状态管理工具
- Redux的根本目的是通过对更新的方式和时间进行规范限制,使得状态的变化变得可预测。它借鉴了“Flux”的思想,将更新逻辑抽离出来,并使用原生JS对象
"action"
来描述需要发生的更改。 - Redux的三大基本原则: 唯一的Store、状态为只读、纯函数来做改变
Redux基本工作流
createStore
createStore是redux的核心函数,主要提供了getState
、subscribe
、dispatch
三个API。其中通过getState
可以读取当前的状态,返回唯一的store;subscribe
是为了订阅监听store的变化,实质就是订阅发布模式,订阅后当store发生变化时通过调用监听函数来通知订阅者。dispatch
函数接收一个名为”action”的参数,redux规定该参数是用来描述变化的原生JS对象,dispatch主要的作用就是将”action”派发给reducer,然后reducer根据当前的状态以及”action”来计算出新的状态,所以真正改变状态是reducer。
根据上面的描述,我们来实现一个简易的createStore。
1 | function createStore(reducer, initialState){ |
上面的代码浅显易懂,现在来测试下这个简易的createStore是否能正常工作。
1 | const initialState = { |
上面我们添加了3个监听函数,当dispatch一个action给reducer,其根据action改变状态后,3个监听函数被依次执行了,合情合理;既然允许订阅,那肯定支持退订了。从代码可以看出subscribe
函数返回值就是退订函数,我们通过调用它即可退订。这里假设我们只想订阅subscribe2一次,代码如下:
1 | function subscribe2(){ |
😫😫😫😫😫
好像出现了问题,第三个订阅函数竟然没有执行。
1 | for (let i = 0; i < listeners.length; i++) { |
现实中,如果我们在listener函数执行时添加或取消listener,不会影响本次dispatch过程,即这些操作只能在下一次dispatch过程中生效;所以我们需要变量来保存下一次的listeners;
1 | function createStore(reducer, initialState){ |
现在用了两个变量来保存监听函数,保证了订阅或取消订阅操作只会在下一次dispatch中生效。我们用上面的示例测试输出如下:
1 | /* console |
😄😄😄😄😄
拆分reducer
Redux坚持唯一store的原则,也就意味着整个应用的state都保存单一的对象中。而我们知道reducer的职责就是根据”action”从旧的state计算出新的state。所以我们需要为应用中发生的所有”action“编写相应的”reducer”,就像的测试示例一样,我们需要在reducer中处理”INCREMENT“和“DECREMENT“,如果还有其它“action”,我们就需要在reducer中继续添加相应的处理逻辑,想象一下如果我们在一个reducer函数中处理整个应用中可能成千上万个“action”,那reducer函数会有多庞大;这显然不合理。所以我们必须拆分reducer,那按照何种方式拆分呢?我们知道尽管所有的state保存在一个对象里面,但其实这些state之间大部分是完全独立的,所以我们可以根据state的独立性来划分reducer,这样每个reducer就可以只需要管理对应的state;
下面我们来管理下两个完全独立的state:counter和todoList;它们有各自的reducer,并且reducer只会管自己对应的state。
1 | // 既然每个reducer分别管理各自部分state,那完全可以把初始值放在reducer里面 |
现在我们已经有了两个reducer,它们能独立更新各自管理的state。那么新的问题来了,我们如何将这两个reducer组合起来确保它们能对派发的action作出响应,各自计算出新的state,并且将这些部分state合并为整个state。这时候我们就需要combineReducers
。
⚠️: 拆分reducer时,我们完全可以将各部分state的初始值放在了对应的reducer中作为默认参数。这样在我们调用createStore时就无需传入初始值了。但是为了能拿到默认初始值,我们必须添加一行代码:
1
2
3
4
5
6
7
8
9 > // ~~~ 省略的代码
> // 在createStore返回前添加
> + dispatch({ type: Symbol() });
> return {
> subscribe,
> dispatch,
> getState
> };
>
这里我们在createStore执行返回前通过dispatch派发一个type为Symbol()的action,这意味着其不会匹配任何reducer里的action,因此所有的reducer都进入default项,即返回默认初始值。
combineReducers
根据上面的问题,我们大致能分析出combineReducers应该是一个函数,它主要的功能就是合并所有的reducer返回一个新的reducer;新的reducer可以接受整个state以及action,并且能把新的state合并为整个store。下面我们尝试实现一个combineReducers:
1 | function combineReducers(reducers){ |
现在我们利用之前的两个reducer来感受下combineReducers的威力。
1 | const reducer = combineReducers({ |
😄😄😄😄😄
actionCreator
Redux强调任何需要改变state的操作都必须通过dispatch派发描述如何改变的action通知reducer更新state来完成,因此项目中往往会大量调用dispatch派发action,而action是一个原生JS对象,其中字段type是与reducer协定的,静态的,而字段payload是动态的,所以我们可以封装一个actionCreator函数来产生action。
1 | function addTodoItem(todoItem){ |
上面的代码里,我们已经封装了两个actionCreator: counterIncrement
、addTodoItem
,我们每次dispatch时可以直接调用它们以产生我们需要的action;尽管这样我们还是需要每次显示调用store.dispatch函数,而且从易用性及扩展性方面上来说,页面组件只需接受相应的状态及改变状态的方法,而不应该感知到Redux的存在。所以我们还需要一个bindActionCreators
函数将dispatch和actionCreator封装起来。
bindActionCreators
1 | function bindActionCreators(anctionCreators, dispatch){ |
结束语
到这里为止,我们已经实现了一个像模像样的Redux!