# Redux概念简述
通过前面的学习,我们已经可以编写简单的页面了。由于React只是一个非常简单的轻量级视图层框架。当一个项目存在非常多的组件的时候,组件间传值将会非常麻烦,代码可维护性非常差。
如果我们想要编写一个大型的项目,我们需要给React搭配一个数据层框架,一般我们使用数据层框架Redux。
# Redux的设计理念
正如上图所示,假设绿色的组件需要向其它组件传值,通过Redux,我们可以将数据存放到一个公共的区域Store
里面,当绿色组件需要改变数据传递给其它组件时,绿色组件只需要更改Store
里面对应的数据就行了。灰色组件会自动感知Store
里的数据变化,重新从Store
里获取数据。这样,绿色组件就很方便地将数据传递给其它组件了。不管组件的层次有多深,他们所走的流程都是一样的。组件改变数据,其它组件重新获取数据,通过这样的设计理念,大大简化了数据传递。
# Redux的构成
Redux的构成实际上可以分为两部分:
Redux = Reducer + Flux
实际上,Flux是React官方提供的最原始的数据层框架,只是在使用过程中发现了一些弊端,因此有人将Flux整合Reducer升级成了Redux。
# Redux的工作流程
在学习使用Redux之前,我们需要了解一下Redux的工作流程。
上图是Redux的工作流程图例。上一节讲过,Redux是一个数据层框架,它把所有的数据都放到了store当中。
每个组件都可以从store中获取和修改数据。所以我们就知道了,React Compents
就是React中的组件,Store
就是存放数据的公共区域。那么Action Creators
和Reducers
代表的是什么呢?为了理解这个问题,我们做如下比喻,将其比作一个图书馆的流程:
React Components
:代表的是一个借书的用户。Action Creators
:代表的是借书时向图书管理员传递的数据。Store
:代表的是图书馆的管理员,管理整个图书馆Reducers
:一个图书馆管理员是无法记住所有的书籍的,Reducers
就相当于一个记录本,记载的是关于图书的信息。
所以整个流程就是:
- 首先,借书人(
React Components
)向图书馆管理员传递的信息(Action Creators
) - 接着,图书馆管理员查阅记录本
Reducers
,Reducers
记录了要借书籍的信息 - 然后,管理员通过书籍信息查询到相应的书籍
- 最后,管理员将借书人需要的书交给借书人
将图书馆流程转换为Redux的工作流程:
- 首先,我有一个组件,这个组件从
Store
中获取所需数据 - 接着,组件通过
Action Creators
传递action给Store
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样式并引入Input
、Button
组件。
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是一个接受参数state
和action
的函数,其中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()
。
为了进一步测试,我们先修改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;
运行项目,可以看到输入框和列表已经成功绑定了数据。
# Action和Reducer的编写
上一节,我们已经完成了创建Store和从Store取数据的功能。这一节我们开始编写Action和Reducer的相关代码。
我们先来安装一个Chrome插件Redux DevTools
。可以通过谷歌访问助手或者其它方式到谷歌商店搜索下载。安装完毕后,打开谷歌开发者工具可以看到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
里的数据一目了然。
前一节,我们已经编写了Store
和Reducer
,让React中的组件能从Store
获取数据,这一节我们尝试改变Store
里的数据。
假设我是一个借书的同学,我需要说一句话(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一起转发给Reducer
,Reducer
根据传递给它的存放在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()
方法订阅Store
。subscribe
方法里可以指定一个函数,当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
的list
参数里并正常展示。因此,我们绑定一个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_value
、add_todo_item
、delete_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.js
和reducer.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
组件中引入并替换掉在handleInputChange
、handleBtnClick
、handleItemDelete
函数中自行创建的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必须是纯函数(纯函数是指给定固定的输入,就一定有固定的输出,且不会有任何副作用)