上篇文章我们谈到了 React 中组件的几种通信方式,如使用 context 及事件订阅模式等,这些模式都很强大方便,但也有一些弊端:数据流难以追踪和管理,当项目复杂度上升,组件之间的关系变得复杂时,这些方式就有点有心无力了。
Redux 就是一款解决状态管理工具,其前身是 FaceBook 提出的 Flux,Redux 在 Flux 的基础上进行了改进,易用性和功能上都有了增强。可以和 React 之类的框架结合使用,方便进行状态管理, 而后在 Redux 基础上推出的 React-Redux 工具,更加适用于 React 的开发模式。
Redux
Redux 有三个重要的概念:
- store:用来统一存放应用中的状态
- action:用来描述 store 修改的对象
- reducer:向 store 派发 action,修改状态
在使用 Redux 的时候,我们可能要应用到下面一些方法:
- createStore:接收 reducer 函数创建 store 对象
- combineReducers:合并多个子 reducer
- store.getState:获取当前 store 上的所有状态
- store.subscribe:监听 store 的变化,接收一个函数作为参数,当 store 中的数据发生变化时,执行该函数
- store.unsubscribe:取消 store 监听
- store.dispatch:派发 action,并根据 action 的 type 类型修改 store 中的数据
基于上面的介绍,我们来实现一个计算器应用。
先搭建计算器的基本结构,创建 Counter.js:
import React,{ Component } from "react";
import "./counter.css";
export default class Counter extends Component{
render(){
return(
<div className = "counter">
<div>
<button>+</button>
<button>-</button>
</div>
<span>当前的值为:0</span>
</div>
);
}
}
App.js:
import React,{ Component } from "react";
import Counter from "./Counter";
export default class App extends Component{
render(){
return(
<div>
<Counter />
</div>
);
}
}
接下来,创建 Redux 相关的文件,为了方便管理,我们把这些文件都放在 Store 文件夹中。
首先,创建 actionTypes.js 文件,用来描述 Action 的 Type:
// 增
export const INCREMENT = "INCREMENT";
// 减
export const DECREAMENT = "DECREAMENT";
创建 actions.js:
import * as ActionTypes from "./actionTypes";
const ACTIONS = {
// 增加 action
increament(){
return{
type:ActionTypes.INCREMENT,
}
},
// 减少 action
decreament(){
return{
type:ActionTypes.DECREAMENT,
}
}
}
export default ACTIONS;
创建 reducers.js,根据 action 的 type 属性来计算状态:
import * as ActionTypes from "./actionTypes";
const reducer = ( state = {
value:0,
}, action) => {
switch( action.type ){
case ActionTypes.INCREMENT:
return{
value:state.value + 1,
}
case ActionTypes.DECREAMENT:
return{
value:state.value - 1,
}
default:
return state;
}
}
export default reducer;
reducer 函数接受两个参数:state 和 action,state 是当前的状态,可以设置默认值。action 就是上面 action.js 中函数执行返回的对象,其中有一个 type 属性,reducer 函数根据该属性返回相应的状态。
reducer 是纯函数,输入相同,输出必定相同,且不能有副作用。因此在 reducer 函数中不能直接修改 state 的值,而是返回一个新的对象,该对象的内容包括变化后的 state 中的值以及其他没有变化的值。
使用 reducer 时需要注意的地方:
- 返回的对象除了应该包括变化的值,还需要包含 state 对象中其他没有变化的值,否则会引起其他组件的状态混乱。实际应用中,通常使用 Object.assign 或者扩展运算符(···)进行对象的合并
- 如果 action 的 type 都不匹配,需要返回一个默认的值(default),一般就返回参数中的 state
新建 store.js:
import { createStore } from "redux";
import reducers from "./reducers";
// 根据 reducers 创建 store 对象
const store = createStore(reducers);
export default store;
修改 Counter.js,应用 Redux 状态管理:
import React,{ Component } from "react";
import store from "./Store/store";
import ACTIONS from "./Store/actions";
import "./counter.css";
export default class Counter extends Component{
constructor(props) {
super(props);
// 获取初始状态
this.state = {
value:store.getState().value,
};
}
componentWillMount() {
// 监听 store 变化
store.subscribe(this.watchStore.bind(this));
}
componentWillUnmount() {
// 对 store 变化取消监听
store.unsubscribe(this.watchStore.bind(this));
}
// 监听回调函数,当 store 变化后执行
watchStore(){
// 回调函数中重新设置状态
this.setState(this.getCurrentState());
}
// 从 store 中获取状态
getCurrentState(){
return{
value:store.getState().value,
}
}
// 增加函数
increase(){
// 派发 INCREMENT Action
store.dispatch(ACTIONS.increament());
}
// 减少函数
decrease(){
// 派发 DECREAMENT Action
store.dispatch(ACTIONS.decreament());
}
render(){
return(
<div className = "counter">
<div>
<button onClick = { this.increase.bind(this) }>+</button>
<button onClick = { this.decrease.bind(this) }>-</button>
</div>
<span>当前的值为:{ this.state.value }</span>
</div>
);
}
}
除了使用 store.getState 获取当前状态之外,我们还需要对 store 的变化进行监听,当 store 发生变化时,执行相应的回调函数,更新组件的状态。
我们可以将监听(subscribe)操作放在 componentDidMount 函数中,将取消监听(unsubscribe)操作放在 componentWillUnmount 函数中。
看下效果:
容器组件和木偶组件(UI组件)
上面的例子中,我们完成了一个基于 Redux 的计算器小应用,接下来对这个应用进行改进。
上面的例子的问题在于:Counter 组件内部的逻辑太复杂了,为了获取 store 中的状态,我们不得不定义一些列的方法,以及 store 监听等。而从计算器应用本身来说,我们想 Counter 组件只展示计算的结果就可以了,不必要内部做过多的逻辑处理。这就引出了容器组件和木偶组件的概念。
- 木偶组件:只负责展示数据的组件,不进行逻辑处理,通过组件的 props 进行展示,自身不维持 state
- 容器组件:负责逻辑处理,并向木偶组件派发 props
我们将 Counter 组件拆成两个组件,容器组件 ContainerCounter 和 UICounter:
ContainerCounter.js:
import React,{ Component } from "react";
import store from "./Store/store";
import ACTIONS from "./Store/actions";
import UICounter from "./UICounter";
export default class ContainerCounter extends Component{
constructor(props) {
super(props);
// 获取初始状态
this.state = {
value:store.getState().value,
};
}
componentWillMount() {
// 监听 store 变化
store.subscribe(this.watchStore.bind(this));
}
componentWillUnmount() {
// 对 store 变化取消监听
store.unsubscribe(this.watchStore.bind(this));
}
// 监听回调函数,当 store 变化后执行
watchStore(){
// 回调函数中重新设置状态
this.setState(this.getCurrentState());
}
// 从 store 中获取状态
getCurrentState(){
return{
value:store.getState().value,
}
}
// 增加函数
increase(){
// 派发 INCREMENT Action
store.dispatch(ACTIONS.increament());
}
// 减少函数
decrease(){
// 派发 DECREAMENT Action
store.dispatch(ACTIONS.decreament());
}
render(){
return(
<UICounter
increase = { this.increase.bind(this)}
decrease = { this.decrease.bind(this)}
value = { this.state.value }
/>
);
}
}
UICounter.js:
import React,{ Component } from "react";
import "./counter.css";
const UICounter = (props) => {
const { increase,decrease,value} = props;
return(
<div className = "counter">
<div>
<button onClick = { increase }>+</button>
<button onClick = { decrease }>-</button>
</div>
<span>当前的值为:{ value }</span>
</div>
);
}
export default UICounter;
执行效果和上面的例子一致。
虽然,我们将 Counter 组件拆分成了两个组件,增加了一点代码量,但是代码的逻辑更加清晰了。也更加符合 React 的思想:一个组件专注做一件事。容器组件用来进行逻辑处理,并向木偶组件传递 props,木偶组件仅负责展示。
总结
本文首先介绍了状态管理工具 Redux,而后基于 Redux 和 React 实现了一个小的计算器,最后介绍了容器组件和木偶组件,并将原始应用中的组件进行了拆分。
关于 Redux 有三个基本要素:
- store
- action
- reducer
以及一些常用的方法:
- createStore
- combineReducers
- store.getState
- store.dispatch
- store.subscribe
- store.unsubscribe
木偶组件和容器组件的引入让我们可以将应用中的表现和逻辑进行分离,使项目更加清晰和容易维护,我们应该总是采取这样的方式。
完。