这个系列分为 3 个部分(这篇文章是第 2 部分):
- 最基本简单的 Redux 实现,以及如何与 React 结合使用
- 实现 React-Redux
- 增强 Redux 的实现,包括拆分合并 reducer,中间件等
react-redux 是 Redux 官方的 React 绑定库。
一个 UI 组件如果需要使用 Redux 来进行状态管理,需要做以下几件事:
- 在组件初始化时内获取 store 中的状态
- 订阅 store 内状态的改变,状态有更新则刷新组件状态
- 在组件卸载时移除对 store 状态的订阅
上面的逻辑,是每个组件与 Redux 结合使用时都需要的。react-redux 将上面的逻辑以高阶组件的形式复用了。
connect
react-redux 的一个基本核心是 connect
函数,它的作用是将一个感知不到 Redux 存在的展示型组件进行包装,将 store 中的状态以及改变状态的能力(dispatch(action)
),注入到组件中。它可能是类似下面形式的函数:
1 | const connect = wrappedComponent => { |
加入上面提到的重复逻辑后:
1 | import React from 'react'; |
同时,考虑到组件可能只需要 store 中的部分 state
和 dispatch
,或者多个组件使用同一个 store,但是所需的 state
和 dispatch
是不一样的,所以可以提供选项给组件来定制使用。为了简明易懂,新增的参数是名为 mapStateToProps
mapDispatchToProps
的 2 个函数。光看函数名字就知道它的作用是什么:
mapStateToProps
选择 store 中的哪些状态传入给组件mapDispatchToProps
选择 store 中的哪些派发动作传入给组件
这里将这 2 个参数定义为函数,是为了获得 wrappedComponent
的作用域,以便访问到 store 实例。以 mapStateToProps
函数为例,它的用法如下:
1 | // state 在被包装组件的作用域内通过 store.getState() 获得 |
类似地,mapDispatchToProps
函数的使用如下:
1 | // dispatch 即在被包装组件作用域内访问到的 store.dispatch |
那么 connect
函数则变成:
1 | // connect.js |
注意上面的 connect.js
文件,存在一个问题:store
是由 Redux 的使用者(同时也是 React-Redux 的使用者)创建的,因此 connect.js
源码文件不可能提前引入 store
。所以我们需要换一种方式,让 Redux 和 React-Redux 的使用者给我们传入 store
这个参数。React 中的数据传递有 2 种方式:
- 通过
props
传递 - 通过 Context API 传递
我们使用 Context API 将 store
进行传递,这样组件树上的每个组件都可以获取到 store
。
Provider
这里使用旧版本的 Context API 来写一个 Provider
组件。
Provider
组件接收一个 store
属性,也就是使用 createStore()
创建的 store。借助 Context API,Provider
的子孙组件可以访问到 store
属性。一般的做法是将应用程序的根组件用 Provider
组件包裹起来:
1 | ReactDOM.render( |
这样,由于 connect
函数处在被包裹组件的作用域内,自然也可以通过 Context API 访问到 store
。
1 | // Provider.js |
借助 Context API,更新后的 connect.js
:
1 | // connect.js |
上面我们实现的 React-Redux 其实还很不完善,这里先根据实际的使用场景,做以下几点简单的改进:
mapStateToProps
和mapDispatchToProps
都应该提供缺省值,因为有的时候某个组件可能只需要 store 的部分状态而不需要派发动作,或者反过来只需要派发动作而不需要状态,这些都是可能的使用场景。我们将参数mapStateToProps
的默认值设为:state => ({})
,这样当用户不需要 store 中的状态时,可以缺省该参数;将参数mapDispatchToProps
的默认值设为dispatch => ({dispatch})
,这样当用户不需要 store 中的派发动作时,可以缺省该参数。connect
和Provider
中的 store 的 PropType 规则可以提取出来,避免代码的冗余。- 目前
connect
返回的组件名都是Connect
,为方便调试,调整组件的静态属性displayName
。 - 目前,我们仅传递了
store.getState()
给mapStateToProps
,但是很可能在筛选过滤需要的状态时,需要依据组件自身的属性进行处理。因此,可以将组件自身的属性以第 2 个参数ownProps
传递给mapStateToProps
,同样的原因,也将自身属性传递给mapDispatchToProps
。 - 在组件内订阅选中的 store 状态时,加入浅比较以优化性能。
简易 React-Redux 代码
需要说明的是,这里的代码,很多的细节和边缘情况没有处理,性能也没有做优化。不过了解 React-Redux 的基本的内部原理应该差不多了。这里为了方便,所有文件都放在同一文件夹里面。核心代码在 connect.js
与 Provider.js
这两个文件内,此外还有 2 个工具属性的文件:浅比较和 store 类型校验。
storeShape.js
1 | import PropTypes from 'prop-types'; |
shallowEqual.js
1 | const shallowEqual = (objA, objB) => { |
connect.js
1 | import React from 'react'; |
Provider.js
1 | import React from 'react'; |