实现 Redux
这个系列分为 3 个部分(这篇文章是第 1 部分):
- 最基本简单的 Redux 实现,以及如何与 React 结合使用
- 实现 React-Redux
- 增强 Redux 的实现,包括拆分合并 reducer,中间件等
Redux 的三大原则:
单一状态树
单一状态树,意味着只有一个 store 对象来保存应用程序的状态。这样的好处包括:易于 debug,实现数据持久化,实现诸如时间旅行等功能。
状态是只读的
状态只能通过触发 action 来改变,action 可以理解为一种改变状态的意图,它是一个类似这样的 JavaScript 对象:
1 | { |
其中的 type
属性是必须要有的,其他的是可选属性。
通过这种形式,所有能改变状态的情形都被集中管理了,并且严格按照一定的顺序来执行。
As actions are just plain objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes.
状态只能通过纯函数来改变
action 是指意图,那么意图的具体内容是什么,则是由 reducer 来指定的。 reducer 是一个纯函数。入参是应用程序的前一个状态,我们不应该修改入参,而是应该返回一个新的状态。一个纯函数必须满足以下两个条件:
- 相同的输入,每次都得到相同的输出。
- 不应该产生副作用,包括但不限于:更改入参,修改局部状态,发起网络请求,读写数据库,触发事件,打印日志等。
纯函数与非纯函数举例:
1 | // 非纯函数,由于 Math.random() 存在,每次得到的输出是不一样的 |
基本实现,不考虑视图层
先不考虑拆分 reducer,中间件,以及一些边缘情况,只考虑最常见的使用情形,Redux 的核心是下面的几个函数:
createStore(reducer)
入参是 reducer,返回一个 store 对象。该对象包含以下 3 个方法getState()
返回一个新的状态对象subscribe(callback)
订阅一个回调函数,当状态变化时,该回调会被执行dispatch(action)
分发 action
Redux 的使用者编写的代码主要是 reducer,来看看 reducer 函数的签名:
1 | const reducer = (state = { todos: [] }, action) => { |
我们试着使用一下上面定义的 reducer:
1 | const state0 = reducer(undefined, { |
现在 state0
的状态如下:
1 | { |
再来一次:
1 | const state1 = reducer(state0, { |
现在 state1
的状态如下:
1 | { |
当然,持续的创建这么多变量来存储某个中间状态没有必要,上面的两步可以用下面的方式实现:
1 | const actions = [ |
注意上面的 reduce
函数的使用,这也是 reducer 被叫做 reducer 的原因。
可以想象,createStore(reducer)
函数的雏形大致如下:
1 | // v0.0 |
尝试下使用我们的 createStore
,传入之前定义的 reducer
:
1 | const store = createStore(reducer); |
按照上面的代码,我们创建的 store
已经可以如预期般运行了,但是还有 2 点需要改进:
第 1 点是每次分发 action 改变了状态后,我们需要手动调用 getState()
才能得到更新后的状态,所以可以考虑在返回的对象中,添加一个订阅函数,每当状态更新后即触发该订阅函数自动执行。
第 2 点是需要对 action 做一些验证,比如 action 必须是一个对象,并且必须包含一个 type
属性等。
实现代码如下:
1 | /////////////////////////////// |
与 React 结合使用
上面的代码订阅了 () => ReactDOM.render()
,每当状态发生变化时,会在页面上重新渲染更新后的状态内容。接着我们更进一步,将 React 组件与上面实现的 Mini Redux 一起使用。另外,Redux 并非只能与 React 一同使用,其他视图库也能与 Redux 结合使用。比如 Vue 自己实现的状态管理库 Vuex 其实跟 Redux 基本是一个东西。
1 | //////////////////////////////// |
好了,一个最简单原始的 Mini Redux 实现了,借助 create-react-app
脚手架,上面的代码应该可以与 React 相结合并正常运行。
但是可以看出,Redux 本身只专注于状态层,负责以可预测的方式集中管理状态。如果需要和视图层绑定,则还有不少工作要做。
以上面的代码为例,我们需要做几件事:
- 在组件初始化时,将组件所依赖的状态(在这里是
todos
和nextTodoId
这两个属性)注入(通过 props 属性的方式传入)到组件本身的状态里。 - 在组件加载后,订阅
store
状态的变化,每当状态更新,就会调用this.setState(this.props.store.getState())
,使得组件状态与store
状态同步。与此同时,将订阅的返回值保存为一个实例this.unsubscribe
,方便取消订阅。 - 每当组件内由于用户交互等方式,需要更新状态时,调用
this.props.dispatch(action)
更新store
内的状态。由于第 2 步的订阅,组件的状态也相应地更新。 - 在组件卸载时,取消订阅。
这些逻辑应该可以被抽取成更加通用的代码以便复用。实际上,React-Redux 这个库就是提供了一套更加通用的方法来实现 Redux 和 React 的绑定。手动实现 React-Redux 见下一篇。