# Redux概念简述

通过前面的学习,我们已经可以编写简单的页面了。由于React只是一个非常简单的轻量级视图层框架。当一个项目存在非常多的组件的时候,组件间传值将会非常麻烦,代码可维护性非常差。

存在与不存在Redux比较.png

如果我们想要编写一个大型的项目,我们需要给React搭配一个数据层框架,一般我们使用数据层框架Redux。

# Redux的设计理念

正如上图所示,假设绿色的组件需要向其它组件传值,通过Redux,我们可以将数据存放到一个公共的区域Store里面,当绿色组件需要改变数据传递给其它组件时,绿色组件只需要更改Store里面对应的数据就行了。灰色组件会自动感知Store里的数据变化,重新从Store里获取数据。这样,绿色组件就很方便地将数据传递给其它组件了。不管组件的层次有多深,他们所走的流程都是一样的。组件改变数据,其它组件重新获取数据,通过这样的设计理念,大大简化了数据传递。

# Redux的构成

Redux的构成实际上可以分为两部分:

Redux = Reducer + Flux

实际上,Flux是React官方提供的最原始的数据层框架,只是在使用过程中发现了一些弊端,因此有人将Flux整合Reducer升级成了Redux。

# Redux的工作流程

在学习使用Redux之前,我们需要了解一下Redux的工作流程。

Redux工作流程

上图是Redux的工作流程图例。上一节讲过,Redux是一个数据层框架,它把所有的数据都放到了store当中。

每个组件都可以从store中获取和修改数据。所以我们就知道了,React Compents就是React中的组件,Store就是存放数据的公共区域。那么Action CreatorsReducers代表的是什么呢?为了理解这个问题,我们做如下比喻,将其比作一个图书馆的流程:

  • React Components:代表的是一个借书的用户。
  • Action Creators:代表的是借书时向图书管理员传递的数据。
  • Store:代表的是图书馆的管理员,管理整个图书馆
  • Reducers:一个图书馆管理员是无法记住所有的书籍的,Reducers就相当于一个记录本,记载的是关于图书的信息。

所以整个流程就是:

  • 首先,借书人(React Components)向图书馆管理员传递的信息(Action Creators)
  • 接着,图书馆管理员查阅记录本ReducersReducers记录了要借书籍的信息
  • 然后,管理员通过书籍信息查询到相应的书籍
  • 最后,管理员将借书人需要的书交给借书人

将图书馆流程转换为Redux的工作流程:

  1. 首先,我有一个组件,这个组件从Store中获取所需数据
  2. 接着,组件通过Action Creators传递action给Store
  3. Store接受action后,从Reducers中获取数据并交由Store返回给组件

若我们需要改变数据也是一样的,组件通过Action Creators将action传递给Store;接着,Store根据情况从Reducers了解到该如何修改数据;当Store完成相应的数据修改后,通知组件重新获取数据;最后,组件从Store重新获取数据。

# 使用Antd实现TodoList

从本节开始,我们将使用React结合Redux重新编写TodoList功能,并且我们还将使用Antd这个UI框架完成一个较为美观的页面。

新建一个分支,删除src目录下除了index.js文件之外的其它文件。

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));

创建TodoList.js文件编写TodoList组件。

首先,我们安装一下这个UI框架:

yarn add antd

接着,我们来编写TodoList组件的提交和输入框按钮。引入antd样式并引入InputButton组件。

import React,{Component} from 'react';
import 'antd/dist/antd.css';
import {Input,Button} from 'antd';

class TodoList extends Component {

    render() {
        return (
            <div style={{marginTop:'10px',marginLeft:'10px'}}>
                <div>
                    <Input placeholder='todo Info' style={{width:'300px',marginRight:'10px'}} />
                    <Button type='primary' >提交</Button>
                </div>
            </div>
        )
    }

}

export default TodoList;

通过上面的代码,我们完成了按钮和输入框结构和样式的编写。

输入框和按钮

然后,我们来编写一下输入框下方的列表。在这里,我们引入使用List组件,设置列表样式。

import React,{Component} from 'react';
import 'antd/dist/antd.css';
import {Input,Button,List} from 'antd';

const data = [
    'Racing car sprays burning fuel into crowd.',
    'Japanese princess to wed commoner.',
    'Australian walks 100km after outback crash.',
    'Man charged over missing wedding girl.',
    'Los Angeles battles huge wildfires.',
  ];

class TodoList extends Component {

    render() {
        return (
            <div style={{marginTop:'10px',marginLeft:'10px'}}>
                <div>
                    <Input placeholder='todo Info' style={{width:'300px',marginRight:'10px'}} />
                    <Button type='primary' >提交</Button>
                </div>
                <List
                    style={{marginTop:'10px',width:'300px' }}
                    bordered
                    dataSource={data}
                    renderItem={item => (<List.Item>{item}</List.Item> )} />
                </div>
        )
    }

}

export default TodoList;

其中,List组件中bordered属性代表是否有边框;dataSource代表数据来源,这里使用data数据;renderItem循环展示列表中的每一项。

列表

通过以上步骤,我们就完成了TodoList组件基本结构的编写。

# 创建Redux的Store

这一节,我们开始编写Redux相关的代码。在Redux中最为重要的是Store,它负责存放组件所用到的所有数据,我们应该优先编写它。

首先,我们先来安装一下Redux。

yarn add redux

其次,我们在src目录下新建store目录并新建一个index.js文件用来编写store代码。

import {createStore} from 'redux';

const store = createStore();

export default store;

通过createStore()方法,我们创建了一个公共区域用于存放数据。

正如在Redux的工作流程一节所比喻的那样:``Store是一个管理员,它需要通过记录本Reducer来帮助其管理图书。因此,我们在Store目录下创建reducer.js`文件。

const defaultState = {
    inputValue:'',
    list:[]
}

export default (state = defaultState,action) =>{
    return state;
}

Reducer是一个接受参数stateaction的函数,其中state代表存放的数据,默认值为defaultState;action代表组件通过Action Creators创建的消息action。

修改一下store目录下的index.js文件,将Reducer引入Store帮助管理数据。

import {createStore} from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

接下来,修改TodoList组件代码

import React,{Component} from 'react';
import 'antd/dist/antd.css';
import {Input,Button,List} from 'antd';
import store from './store/index.js';

class TodoList extends Component {

    constructor(props) {
        super(props);
        console.log(store.getState());
    }

    render() {
        return (
            <div style={{marginTop:'10px',marginLeft:'10px'}}>
                <div>
                    <Input  placeholder='todo Info' style={{width:'300px',marginRight:'10px'}} />
                    <Button type='primary' >提交</Button>
                </div>
                <List
                    style={{marginTop:'10px',width:'300px' }}
                    bordered
                    dataSource={[]}
                    renderItem={item => (<List.Item>{item}</List.Item> )} />
                </div>
        )
    }

}

export default TodoList;

TodoList组件中,我们引入Store,删除data常量,将列表设为空数组;增加构造函数并打印store.getState()

store功能

为了进一步测试,我们先修改reducer.js文件,给defaultState中的变量赋予默认值。

const defaultState = {
    inputValue:'123',
    list:[1,2]
}

export default (state = defaultState,action) =>{
    return state;
}

然后,在构造函数中将store.getState()中数据赋值this.state并将this.state中对应的值分别绑定到输入框和列表中。

import React,{Component} from 'react';
import 'antd/dist/antd.css';
import {Input,Button,List} from 'antd';
import store from './store/index.js';

class TodoList extends Component {

    constructor(props) {
        super(props);
        this.state = store.getState();
        console.log(this.state);
    }

    render() {
        return (
            <div style={{marginTop:'10px',marginLeft:'10px'}}>
                <div>
                    <Input value={this.state.inputValue} placeholder='todo Info' style={{width:'300px',marginRight:'10px'}} />
                    <Button type='primary' >提交</Button>
                </div>
                <List
                    style={{marginTop:'10px',width:'300px' }}
                    bordered
                    dataSource={this.state.list}
                    renderItem={item => (<List.Item>{item}</List.Item> )} />
                </div>
        )
    }

}

export default TodoList;

运行项目,可以看到输入框和列表已经成功绑定了数据。

store返回数据

# Action和Reducer的编写

上一节,我们已经完成了创建Store和从Store取数据的功能。这一节我们开始编写Action和Reducer的相关代码。

我们先来安装一个Chrome插件Redux DevTools。可以通过谷歌访问助手或者其它方式到谷歌商店搜索下载。安装完毕后,打开谷歌开发者工具可以看到Redux标签。

谷歌插件Redux开发工具

Redux标签提示没有找到store,点击the instructions进行配置,它需要我们在创建store时在createStore方法指定第二个参数的值为window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()

import {createStore} from 'redux';
import reducer from './reducer';

const store = createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

export default store;

这句话表示若存在Redux DevTools扩展则启用该扩展。重新刷新页面,Redux标签下出现如下面板,选择State子标签页,Store里的数据一目了然。

谷歌插件Redux开发工具面板

前一节,我们已经编写了StoreReducer,让React中的组件能从Store获取数据,这一节我们尝试改变Store里的数据。

Redux工作流程

假设我是一个借书的同学,我需要说一句话(action),这句话由Action Creators帮忙创建,我们先把Action Creactors忽略,先创建这句话(即action)。

当我们改变输入框中的内容时,我们希望Store中的inputValue值也跟着变,因此我们对输入框绑定一个change事件。

 <Input value={this.state.inputValue} placeholder='todo Info' style={{width:'300px',marginRight:'10px'}}
                    onChange ={this.handleInputChange} />

在这个处理事件函数中,我们定义一个action,指定其类型以及要传递的值。Store提供了dispatch方法,通过该方法我们可以将action传递给Store。

 handleInputChange(e) {
        const action ={
            type:'change_input_value',
            value: e.target.value
        }
        store.dispatch(action);
    }

前面我们说过,Store相当于一个管理员,需要借助Reducer才能知道如何处理数据。当Store接收到action后,将把Store存放的值和action一起转发给ReducerReducer根据传递给它的存放在Store中的值和当前的action做出相应的处理并返回新的值给Store

if(action.type === 'change_input_value') {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
}

在这段代码中,我们深拷贝了state数据newState,并使用action中传递的value替代了newState中的inputValue值。

提醒

reducer可以接受state,但不能修改state数据。

Store会接受Reducer返回的数据并用返回的数据替代存储的旧数据。

我们测试一下代码的正确性。在输入框中改变内容为"123a"。

改变输入框中的数据

可以看到Store中的值已经改变,但是输入框中的内容没有变,即组件并没有更新。因此,我们需要在TodoList组件的构造函数方法中调用store.subscribe()方法订阅Storesubscribe方法里可以指定一个函数,当Store里的数据发生改变时,从Store里重新获取数据并更新组件数据的值。

 constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        store.subscribe(this.handleStoreChange);
}

其中,handleStoreChange方法用来更新组件的数据。

handleStoreChange() {
   this.setState(store.getState());
}

再次进行测试,此时输入框和Store里的数据产生了联动效果。

store和输入框数据同步

同理,我们希望点击提交按钮时,数据能够存到Storelist参数里并正常展示。因此,我们绑定一个Click事件。

<Button type='primary' onClick={this.handleBtnClick} >提交</Button>

接着,我们定义handleBtnClick函数,和前面一样创建一个action,并将action发送给Store

handleBtnClick() {
   const action = {
     type:'add_todo_item'
   }
   store.dispatch(action);
}

Store接收到action后,将先前存储的数据和action一起发给Reducer处理。同样地,我们深拷贝先前的数据,更新参数list并将输入框参数inputValue的值置空。

  if(action.type === 'add_todo_item') {
        const newState =  JSON.parse(JSON.stringify(state));
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
  }

测试代码,输入内容并点击提交按钮,列表成功添加内容。

提交输入框内容到列表

# 使用Redux完成TodoList

在前面的章节中,我们已经完成了TodoList输入内容、添加内容的功能,这一节我们开始完成TodoList删除功能。

首先,我们先修改一下reducer.js文件中state参数的默认值,将inputValue设置为空字符串、list设置为空数组。

const defaultState = {
    inputValue:'',
    list:[]
}

接着,我们在TodoList组件的List.Item组件上绑定一个Click事件,并传递一个index参数给事件函数。

 <List
        style={{marginTop:'10px',width:'300px' }}
        bordered
        dataSource={this.state.list}
        renderItem={(item,index) => (<List.Item onClick={this.handleItemDelete.bind(this,index)}>{item}</List.Item> )} />

通过handleItemDelete事件函数,我们发送一个action给Store

handleItemDelete(index) {
     const action = {
        type:'delete_todo_item',
        index
     }
     store.dispatch(action);
}

Store接收到action后又把它转发给Reducer,因此,我们在reducer.js文件中增加一段代码,用于删除被点击的列表项。

if(action.type === 'delete_todo_item') {
    const newState =  JSON.parse(JSON.stringify(state));
    newState.list.splice(action.index,1);
    return newState;
}

# ActionTypes的拆分

通过前面的代码,我们已经完成了TodoList的所有功能,现在我们逐步对其进行优化。在TodoList组件和Reducer中我们使用了类似change_input_valueadd_todo_itemdelete_todo_item的字符串。其实这样做是非常不好的,若我们不幸写错了字符串的字母,程序不会报错,查找错误非常困难。

为此,我们在目录store下创建actionTypes.js文件对ActionTypes进行拆分,将字符串定义在一个文件中。当我们写错了字符串字母时,程序会报错,有利于我们查找错误。

export const CHANGE_INPUT_VALUE = 'change_input_value';
export const ADD_TODO_ITEM = 'add_todo_item';
export const DELETE_TODO_ITEM = 'delete_todo_item';

接着·,我们分别在TodoList.jsreducer.js文件中引入ActionTypes。

引入TodoList.js文件

import React,{Component} from 'react';
import 'antd/dist/antd.css';
import {Input,Button,List} from 'antd';
import store from './store/index.js';
import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from './store/actionTypes';

class TodoList extends Component {

    constructor(props) {
        super(props);
        this.state = store.getState();
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleStoreChange = this.handleStoreChange.bind(this);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        store.subscribe(this.handleStoreChange);
    }

    render() {
        return (
            <div style={{marginTop:'10px',marginLeft:'10px'}}>
                <div>
                    <Input value={this.state.inputValue} placeholder='todo Info' style={{width:'300px',marginRight:'10px'}}
                    onChange ={this.handleInputChange} />
                    <Button type='primary' onClick={this.handleBtnClick} >提交</Button>
                </div>
                <List
                    style={{marginTop:'10px',width:'300px' }}
                    bordered
                    dataSource={this.state.list}
                    renderItem={(item,index) => (<List.Item onClick={this.handleItemDelete.bind(this,index)}>{item}</List.Item> )} />
                </div>
        )
    }

    handleInputChange(e) {
        const action ={
            type: CHANGE_INPUT_VALUE,
            value: e.target.value
        }
        store.dispatch(action);
    }

    handleStoreChange() {
        this.setState(store.getState());
    }

    handleBtnClick() {
        const action = {
            type: ADD_TODO_ITEM
        }
        store.dispatch(action);
    }

    handleItemDelete(index) {
       const action = {
           type: DELETE_TODO_ITEM,
           index
       }
       store.dispatch(action);
    }

}

export default TodoList;

引入reducer.js文件

import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from './actionTypes';
const defaultState = {
    inputValue:'',
    list:[]
}

export default (state = defaultState,action) =>{
    if(action.type === CHANGE_INPUT_VALUE) {
        const newState = JSON.parse(JSON.stringify(state));
        newState.inputValue = action.value;
        return newState;
    }
    if(action.type === ADD_TODO_ITEM) {
        const newState =  JSON.parse(JSON.stringify(state));
        newState.list.push(newState.inputValue);
        newState.inputValue = '';
        return newState;
    }
    if(action.type === DELETE_TODO_ITEM) {
        const newState =  JSON.parse(JSON.stringify(state));
        newState.list.splice(action.index,1);
        return newState;
    }

    return state;
}

# 使用Action Creators统一创建action

在前面的章节中,我们没有使用Action Creators统一创建action,而是让函数自行创建action。对于小型项目来说这是可行的,对于大型项目则会带来日后代码管理上的不便。

首先,我们先在目录store下创建一个actionCreators.js文件,并创建三种不同类型的action对象。

import {CHANGE_INPUT_VALUE,ADD_TODO_ITEM,DELETE_TODO_ITEM} from './actionTypes';

export const getInputChangeAction = (value) => ({
    type: CHANGE_INPUT_VALUE,
    value
});

export const getAddItemAction = () => ({
    type: ADD_TODO_ITEM
});

export const getDeleteItemAction = (index) => ({
    type:DELETE_TODO_ITEM,
    index
});

其次,我们在TodoList组件中引入并替换掉在handleInputChangehandleBtnClickhandleItemDelete函数中自行创建的action。

    handleInputChange(e) {
        const action = getInputChangeAction(e.target.value);
        store.dispatch(action);
    }

    handleStoreChange() {
        this.setState(store.getState());
    }

    handleBtnClick() {
        const action = getAddItemAction();
        store.dispatch(action);
    }

    handleItemDelete(index) {
       const action = getDeleteItemAction(index);
       store.dispatch(action);
    }

这样,我们就能使用Action Creators统一创建action了。

# Redux设计和使用三原则

在前面的章节中,Redux基础知识都覆盖到了。下面我们来补充一下Redux设计和使用三原则。

  • store必须是唯一的
  • 只有store才能改变自己的内容
  • Reducer必须是纯函数(纯函数是指给定固定的输入,就一定有固定的输出,且不会有任何副作用)
LastUpdated: 7/9/2020, 9:08:09 AM