Redux有点类似于Vuex,是针对一个单页面应用统一的状态管理. 如果各个组件都需要保存自己的state, 而这些state之间互相需要同步, 那么一个潜在而且非常重要的问题就是万一组件的state没有得到同步会怎么样. 与其将状态保存在各个组件, 然后互相通信来进行更新, 可以方便的采取一个统一的状态管理, 这就是Redux的特点. Redux的特点就是单向数据流, 所有的操作都从定义好的一个Action开始, 每个Action决定了要对状态库进行何种操作, 具体操作由Reducer决定, 然后将Action注册在状态库中, 每次只向库提交Action, 库更新成功后, 就会驱动渲染引擎去更新页面. 听上去, Vuex和Redux的比较类似, 不过Redux的特点如下:
唯一数据源, 即一个单页面应用, 使用一个Store就可以了
保持状态只读, 更新状态只通过一个Reduce
数据改变只准通过纯函数完成, 纯函数就是一个Reducer.
Redux的理念
Redux的业务循环如下:
首先需要创建一个reducer, 这个reducer就是用来更新Store的纯函数, 这个函数签名如下:reducer(state, action), 其中的state是当前的状态, Action可以理解为一个要更新状态的动作, 这个纯函数要做的事情就是根据state和action来返回一个新的state. 这个新的state也就是更新后的Store中存储的当前应用的新状态.
其次需要创建一个Store对象, 这个就是整个单页面应用的状态库, 这个Store对象通过Redux库的createStore来创建, 其中的构造器需要两个参数, 一个是reducer, 一个是初始化的数据(可以用JS对象来表示), 相当于为其注册了一个reducer.
然后就是定义Action对象, 一般来说, Action对象就是一个JS对象, 其中应该带有一个标识, 用来表示当前的Action对象要如何更新state(比如加减某个变量, 更新其他等等), 好让reducer决定如何来处理新的state.
最后给要更新store的组件, 添加上更新store的事件代码, 也就是store.dispatch(Action), 即将Action使用store.dispatch发送给store. store就会使用之前注册好的reducer, 接受这个Action, 进而来更新整个状态. 通过事件来更新Store中的状态, 这个所有组件都可以使用, 所以就形成了统一的状态管理.
使用Redux的小程序
这里搞个小例子来写一下, 比如输入三个数值, 来改变一块区域的颜色. 首先需要设计一下, 需要一个独立的区域, 让其的颜色需要等于store中存储的三种颜色. 然后有另外三组按钮, 每一组有两个按钮, 用于改变RGB三种颜色中的一种, 增加或者减少. 这里的核心就是, 每一个input+按钮做成一个组件, 这个组件必须更新store中的某一个颜色, 如何知道更新哪一个颜色呢, 就需要通过Action中的一个属性来确定. 启动一个新的React项目, 安装好Redux, 然后来创建一系列组件, 在src目录下创建page, store, component三个目录. 先来看各种组件, HomePage.js如下:
import React from "react";
import './HomePage.scss';
import TargetArea from "../component/TargetArea";
import ControlButtons from "../component/ControlButtons";
const HomePage = () => {
return (
<div className='wrapper'>
<h1>测试改变区域颜色</h1>
<TargetArea />
<div className='buttonWrapper'>
<ControlButtons/>
<ControlButtons/>
<ControlButtons/>
</div>
</div>
)
};
export default HomePage;
TargetArea组件如下:
import React from "react";
import './TargetArea.scss';
const TargetArea = () => {
return (
<div className='target'/>
)
};
export default TargetArea;
ControlButtons组件如下:
import React from "react";
import './ControlButtons.scss';
class ControlButtons extends React.Component {
constructor(props) {
super(props);
}
render() {
console.log(this.props);
return (
<div className='buttons'>
<button>+</button>
<button>-</button>
</div>
);
}
}
export default ControlButtons;
组件很简单, ControlButton和TargetArea还是同级别的组件, 这之间都没有什么交互, 然后要通过Redux来进行交互, 具体的做法就是三组button需要修改store中的三种颜色. 而且要限制于0-255之间. 修改之后的颜色, 会作用到TargetArea上来, 看一下如何来给这个项目添加上Redux.
为按钮添加更新Store的事件
Action就是一个JS对象, 标识需要对Store进行什么操作, 所以其中得有一种标识符用来标识类型, 一般的做法是, 将标识符单独弄一个JS文件, 不同类型的Action对应着不同的操作. 在src下新创建一个store目录, 所有redux相关的代码都放在其中, 在内部创建一个ActionTypes.js:
export const REDPLUS = 'REDPLUS';
export const REDMINUS = 'REDMINUS';
export const GREENPLUS = 'GREENPLUS';
export const GREENMINUS = 'GREENMINUS';
export const BLUEPLUS = 'BLUEPLUS';
export const BLUEMINUS = 'BLUEMINUS';
这个其实代表了6种不同的事件, 分别是RGB的增加和减少, 一共六种情况. 然后就可以来创建Action.js, 每个Action实际上就是返回一个对象, 一个对象中至少有一个标志ActionType的字段, 另外的数据字段可以自己选择, 由于一共就6种情况, 所以可以不需要额外的字段.
import * as ActionTypes from './ActionTypes';
export const redIncrement = () =>{
return {
type: ActionTypes.REDPLUS
}
}
export const redDecrement = () =>{
return {
type: ActionTypes.REDMINUS
}
}
export const greenIncrement = () =>{
return {
type: ActionTypes.GREENPLUS
}
}
export const greenDecrement = () =>{
return {
type: ActionTypes.GREENMINUS
}
}
export const blueIncrement = () =>{
return {
type: ActionTypes.BLUEPLUS
}
}
export const blueDecrement = () =>{
return {
type: ActionTypes.BLUEMINUS
}
}
这实际就是六个函数返回6个不同的Action, 其中标记了这个Action的类型. 然后来创建Store和Reducer.js, 这两个文件其实需要交替来编写, 首先是Store.js:
import {createStore} from "redux";
const initValue = {
red:128,
green:128,
blue: 128
}
接下来创建Store对象的时候, 必须要编写reducer, 所以来编写Reducer.js:
import * as ActionTypes from './ActionTypes';
const reducer = (state, action) =>{
switch (action.type) {
case ActionTypes.REDPLUS:
if (state.red === 255) {
return {...state};
} else {
return {...state, red: state.red + 1}
}
case ActionTypes.REDMINUS:
if (state.red === 0) {
return {...state};
} else {
return {...state, red: state.red - 1}
}
case ActionTypes.GREENPLUS:
if (state.green === 255) {
return {...state};
} else {
return {...state, green: state.green + 1}
}
case ActionTypes.GREENMINUS:
if (state.green === 0) {
return {...state};
} else {
return {...state, green: state.green - 1}
}
case ActionTypes.BLUEPLUS:
if (state.blue === 255) {
return {...state};
} else {
return {...state, blue: state.blue + 1}
}
case ActionTypes.BLUEMINUS:
if (state.blue === 0) {
return {...state};
} else {
return {...state, blue: state.blue - 1}
}
default:
return {...state};
}
}
这个Reducer的意思是, 根据不同类型的Action, 对旧的state进行更新, 但是不是更改旧的state, 而是返回一个新的state对象, 因此这里根据不同类型的动作, 将原来的state拆解到新的对象中, 然后更新了对应各个事件的减少或者增加值的结果, 返回的是一个新的对象. 这个对象将被替代成为新的Store中的state, 如果超出范文, 则不会更新state. 有了Reducer之后, 就可以来继续编写Store.js了:
import {createStore} from "redux";
import reducer from "./Reducer";
const initValue = {
'red': 128,
'green': 128,
'blue': 128
}
const store = createStore(reducer, initValue);
export default store;
这里使用reducer和初始化的值创建了一开始的Store, 也是这个APP里唯一的状态库, 有了这个Store之后, 剩下的就是来给按钮组件添加上事件:
import React from "react";
import './ControlButtons.scss';
import store from "../store/Store";
import * as Actions from '../store/Action';
class ControlButtons extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className='buttons'>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
);
}
increment = ()=>{
switch (this.props.caption) {
case 'red':
store.dispatch(Actions.redIncrement());
break;
case 'green':
store.dispatch(Actions.greenIncrement());
break;
case 'blue':
store.dispatch(Actions.blueIncrement());
break;
default:
}
}
decrement = ()=>{
switch (this.props.caption) {
case 'red':
store.dispatch(Actions.redDecrement());
break;
case 'green':
store.dispatch(Actions.greenDecrement());
break;
case 'blue':
store.dispatch(Actions.blueDecrement());
break;
default:
}
}
}
export default ControlButtons;
注意红色的两个事件, 每按下增加或者减少按钮, 根据当前的按钮从父组件得到的caption, 组件可以知道自己对应的是六个事件中的哪一个. 知道了之后, 调用Actions中的六个方法之一, 生成对应的Action对象, 然后使用store.dispatch()来把Action提交给Store用于更新状态. 如果在reducer中加上打印语句就可以得到改变前的state用于检查是否生效. 这里成功的更改了store中的red, green, blue三个键对应的值.
更改目标区域的颜色
现在通过组件, 已经可以根据不同的事件类型来更新Store了, 下一步就是需要将store的内容渲染到TargetArea这个组件中, 也就是读取State的状态并且进行显示. 这里有个最大的问题就是, 如何根据store的变化来同时更新目标区域的颜色呢? store对象有一个subscribe()方法, 其中可以传入一个回调函数, 只要store状态更新, 就会调用这个方法. 同样也有一个unsubscribe用于取消订阅, 有了这两个方法, 搭配上store.getState()方法, 就可以被动的获取更新后的Store状态. 根据这个思路, 只需要在每次store变动的时候, 从中出去RGB三原色的值, 然后使用字符串拼接成一个属性传递给当前的组件的style属性就可以了:
import React from "react";
import './TargetArea.scss';
import store from "../store/Store";
class TargetArea extends React.Component {
constructor(props) {
super(props);
this.state = {
red: 128,
green: 128,
blue: 128
}
}
// 给store订阅的回调函数, 一旦有变化, 就更新自己的state与Store一致
subscribeToStore = () =>{
this.setState(store.getState());
}
//组件挂载完成后就要订阅
componentDidMount() {
store.subscribe(this.subscribeToStore);
}
//组件生命周期结束的时候取消订阅
componentWillUnmount() {
store.unsubscribe(this.subscribeToStore)
}
render() {
const style = {'backgroundColor': `rgb(${this.state.red},${this.state.green},${this.state.blue})`}
return (
<div style={style} className='target'/>
);
}
}
export default TargetArea;
这里的核心就是注册这个回调函数, 然后在更新的时候, 从store中取出当前的状态更新到自己的状态中, 这样可以时刻保持子组件与唯一状态库store一致. 而且这里也通过具体6种事件之一, 实现了子组件之间的互相通信.