很多时候,用是知道怎么用的,但其中的逻辑和原理却不甚了解,多看,多理解。
Redux 基本原则
针对 Flux 这种框架理念,有很多种实现,比如 Redux、Reflux、Fluxible 等(redux 只是其中之一哦!),不过 Redux 有许多优势以及针对 flux 不足作出的优化。Flux 的基本原则是「单向数据流」,Redux 在此基础上强调了三个原则,介绍如下。
数据源唯一
其实就是指把所有状态数据放到唯一的一个 store 上。为什么呢?因为多个 store 首先会有潜在的数据冗余的问题,难以保证数据一致性;其次多个 store 的存在容易产生依赖问题,虽然最终能有解决方案(eg. Flux 的 waitFor),但依赖关系的存在增加了应用的复杂度,容易带来新问题。
Redux 中这个唯一的 store 上的状态是一个树形的对象,每个组件只是其中一部分的数据,后面再讲如何设计。
状态只读
状态只读并不意味着状态不能改(要是不能改的话,UI=render(state)就完全不会变了),只是说我状态的修改不是直接改状态上的值,而是创建新的状态对象给 Redux,由 Redux 完成新状态的组装。
数据改变只能通过纯函数完成
这个「改变数据」的「纯函数」就是规约函数 Reducer 啦,Redux 的起名也正代表了 Reducer + Flux。普通函数的使用举例如下,iterator 类型数据有 reduce 函数,它可以对元素依次进行规约,得到一个目标。
1 | // ⬇上次规约结果 ⬇本次规约元素 |
Redux 中对 Reducer 的使用也类似,参数中上次规约的结果就是上个状态 state,本次规约的元素则是此时接收到的 action 对象,函数要做的就是无副作用(纯函数)地根据它俩返回新的状态,可以对比下 Flux 中的处理,就能理解「副作用」。
1 | // Flux 中的处理 |
这里我们的 reducer 只负责「计算状态」,而不用负责「存储状态」,这也是和 Flux 有区别的地方。Redux 本身并没有额外强大的功能,反而像是给 Flux 添加了许多限制,不过正是这种限制才更加有利我们开发。
Redux 基础例子
用 Redux 重构下之前的例子,看看 Redux 的特别之处吧!
action
和 Flux 类似,把 action 分为类型和构造函数两个文件,前者类似,后者则大不一样。Flux 函数中创建了 action 并立刻 dispatch(传送门),Redux 中则只是纯粹地返回了一个 action 对象。
1 | // src/Actions.js |
dispatcher ✕ createStore √
Flux 中需要全局定义一个 Dispatcher(传送门),Redux 中则没有这个东西。Dispatcher 存在的作用就是把一个 action 对象分发给多个注册了的 Store,既然 Redux 让全局只有一个 Store,那么再创造一个 Dispatcher 就意义不大了。所以 Redux 中将分发这一功能,从一个 Dispatcher 对象简化为 Store 对象上的一个函数 dispatch,毕竟只有一个 Store,要分发也是分发给这个 Store,那我就调用 Store 上一个表示分发的函数就行了,非常合理。
1 | // src/Store.js |
这里的 createStore 接收两个参数,第一个表示更新状态的 reducer,第二个表示初始状态,第三个是个可选的代表 Store Enhancer 的参数(后面讲)。
这里我们传入的 reducer 就是上面提到过的那个,它和 Flux 中 Store 注册的回调函数类似,都是一个 switch 语句,不同之处在于 Redux 额外传入了 state,把 state 的存储交给 Redux 框架本身,让 reducer 只关心如何更新 state。PS: redux 中一定不要修改这个参数 state,这不好。
view
顶层组件 src/views/ControlPanel.js
的代码和 Flux 那边一样(传送门),具体的 src/views/Counter.js
则不一样,区别有许多。
首先是初始化时 state 的来源不同、store 的数量不同。
1 | // src/views/Counter.js |
其次是保持状态同步,这里事件监听可以用 store 原生的。(其实这边 mount 的 subscribe 代码可以写到 constructor 中,单独拎到 did mount 中只是为了和 unmount 对称而已)。
1 | onChange() { |
再然后是触发状态变动,这里区别在于 action 构造函数,Flux 中构造同时派发,Redux 中仅构造,因此需要先调用构造函数,再调用 store 的 dispatch 方法。
1 | onIncrement() { |
再看下 Summary 组件,这里要根据唯一的 store 中状态进行计算。
1 | // src/views/Summary.js |
改进一:容器组件和傻瓜组件
上面的每个组件都完成了两件事情,一个是和 redux store 打交道(初始化、监听 store 改变、触发 store 改变),另一个是根据当前 props 和 state 渲染 UI。为了实践专一性原则,我们可以根据上述功能将组件划分为两个组件。在这种划分维度下,二者是父子组件的关系,其中和 redux 打交道的部分我们称之为外层的「容器组件」(Container Component),专注渲染的部分我们称之为内层的「傻瓜组件」(Dumb Component)。(不过聪明和笨也是相对的,外层虽要处理各种状态,但也都是有套路、可抽离复用的;内层虽只负责渲染,但其纯函数的性质还蛮有大智若愚之感的)。
其实还蛮好理解的,把状态相关都丢给外层,把视图相关都丢给内层,二者通过 props 来传递数据即可。(拆分其实和 Redux 无关,Flux 中也有,这里提及只是为了引出后面的 react-redux 库)。
拿上面的 Counter 组件来举个例子拆分下吧。
1 | // src/views/Counters.js 傻瓜组件 |
傻瓜组件完全木有 state,所有数据都来自 props,也只有一个 render 函数,被称作「无状态组件」。
1 | // src/views/Container.js |
容器组件的 render 只需要负责渲染傻瓜组件即可,外层调用者看到的只是容器组件,并不关心傻瓜组件。Summary 组件的拆分也类似。
改进二:组件 Context
Redux 中只有一个 Store,我们在许多地方都通过 import 拿到了它,但这种导入方式会有问题。比如当我们定义一个组件时,或者使用 npm 引入第三方组件时,我们不知道组件会在哪里被使用,因此也无法预先知道唯一 Store 的定义位置了,这种直接导入的方式不利于组件复用。
最好的做法是,只有一个地方直接导入 Store,其余组件避免直接导入 Store。对于前者,很容易想到把最顶层 React 组件作为导入处;对于后者,我们能想到最直接的获取 store 的方法,就是通过 props。通过 props 的方式需要层层传递,很麻烦,这时就不得不说 React 提供的 Context 功能了。
Context 是上下文环境,它能让一个树状组件上「所有组件」都能「访问一个共同的对象」。它需要上级组件和下级组件配合使用,前者要声明自己支持 context,提供一个返回 context 的函数;后者要宣称自己要使用 context,通过 this.context 访问这个共同环境的对象。
综合改进(手写版)
综合起来,我们的目标是「在顶层容器引入 store、声明 context」供「所有子组件通过 context 直接取用 store」。每个应用的顶层元素都不一样,因此我们把它抽象成一个特殊的 React 组件,作为通用的 context 提供者,我们称之为「Provider」。
如何实现 Provider 呢?
1 | // src/Provider.js |
顶层组件如何使用 Provider 呢?
1 | // src/index.js |
傻瓜组件如何使用 Provider 呢?
1 | // src/views/Counter.js |
容器组件如何使用 Provider 呢?
1 | // src/views/CounterContainer.js,其他代码见上方 #改进一 处 |
总结一下,React 的 Context 相当于一个全局对象,全局对象容易在不经意的地方被改变,按理说应该避免使用。但由于 Redux 的 Store 封装得很好,没有提供直接修改状态的功能,部分地克服了作为全局对象的缺点;且 Redux 中一个应用只有一个 Store,也防止了滥用的情况,因此,使用 Context 来传递 Store 是一个不错的选择。
综合改进(调库版)
上面通过一个手写的 Provider + 组件拆分实践了两种改进方法(还记得是哪两种吗?),不难发现这其中是有套路的,把套路抽象出来复用,就成了库,有个很不错的库,叫 react-redux。
react-redux 对上述两种改进方法的实践,分别如下:
- connect:连接容器组件和傻瓜组件
- Provider:提供包含 store 的 context
下面分别介绍下 react-redux 提供的这俩功能。
connect
我们的手工实践中,自己定义了一层 CounterContainer,这个库则没有定义此组件,而是直接导出了如下语句。
1 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); |
这里的 connect 函数接收两个参数,返回一个新函数;新函数接收一个参数,返回容器组件。想知道 connect 的参数含义,就要从「理解容器组件做了什么事」来入手,也就是通过目的反推实现起来需要的依赖,那么容器组件做了啥呢?
- 把 store 上的状态转化为内层傻瓜组件的 props
- 把内层傻瓜组件的用户行为转化为派送给 store 的动作
其实两边都是映射,前者是傻瓜组件的输入,是 state 到 props 的映射(外层 state 以 props 中变量的形式出现在内层);后者是傻瓜组件的输出,是 dispatch 到 props 的映射(外层 dispatch 以 props 中函数的形式出现在内层)。知道了这俩,就很好理解 connect 中传递的 mapStateToProps 和 mapDispatchToProps 了,如下。
1 | // 外层 state 中的数据,取出 key 为内层 props 的 caption 值的值,作为内层 props 的 value 值 |
1 | // 内层多了两个 props:OnIncrement、OnDecrement |
Provider
react-redux 的 Provider 和我们之前手写的几乎一样,不过更加严谨了些。我们只限制 store 属性是一个 object,react-redux 中则要求 store 不光是一个 object,还必须要包括三个函数:subscribe、dispatch、getState。
除此之外,react-redux 定义了 Provider 的 componentWillReceiveProps(快回忆下它的作用!传送门),此生命周期函数会在每次重新渲染的时候被调用到,react-redux 在这个生命周期函数中会检查此次渲染时代表 store 的 props 和上一次是否一样,如果不一样则警告,避免多次渲染用了不同的 Redux Store。每个应用只能有一个 Redux Store,整个 Redux 的生命周期中都应该保持 Store 的唯一性。
以上就是简单的 redux 知识 + react-redux 知识了!后面要温习的还有很多,下次继续吧~