Redux有点类似于Vuex,是针对一个单页面应用统一的状态管理. 如果各个组件都需要保存自己的state, 而这些state之间互相需要同步, 那么一个潜在而且非常重要的问题就是万一组件的state没有得到同步会怎么样.

与其将状态保存在各个组件, 然后互相通信来进行更新, 可以方便的采取一个统一的状态管理, 这就是Redux的特点.

Redux的特点就是单向数据流, 所有的操作都从定义好的一个Action开始, 每个Action决定了要对状态库进行何种操作, 具体操作由Reducer决定, 然后将Action注册在状态库中, 每次只向库提交Action, 库更新成功后, 就会驱动渲染引擎去更新页面.

听上去, Vuex和Redux的比较类似, 不过Redux的特点如下:

  1. 唯一数据源, 即一个单页面应用, 使用一个Store就可以了
  2. 保持状态只读, 更新状态只通过一个Reduce
  3. 数据改变只准通过纯函数完成, 纯函数就是一个Reducer.
  1. Redux的理念
  2. 使用Redux的小程序
  3. 为按钮添加更新Store的事件
  4. 更改目标区域的颜色

Redux的理念

Redux的业务循环如下:

  1. 首先需要创建一个reducer, 这个reducer就是用来更新Store的纯函数, 这个函数签名如下:reducer(state, action),
    其中的state是当前的状态, Action可以理解为一个要更新状态的动作, 这个纯函数要做的事情就是根据state和action来返回一个新的state. 这个新的state也就是更新后的Store中存储的当前应用的新状态.
  2. 其次需要创建一个Store对象, 这个就是整个单页面应用的状态库,
    这个Store对象通过Redux库的createStore来创建, 其中的构造器需要两个参数, 一个是reducer, 一个是初始化的数据(可以用JS对象来表示), 相当于为其注册了一个reducer.
  3. 然后就是定义Action对象, 一般来说, Action对象就是一个JS对象, 其中应该带有一个标识, 用来表示当前的Action对象要如何更新state(比如加减某个变量, 更新其他等等), 好让reducer决定如何来处理新的state.
  4. 最后给要更新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种事件之一, 实现了子组件之间的互相通信.