为什么要使用Redux
我们知道React是一个简单的视图层框架,React开发出的应用是由许多组件构建而成,如果我们开发的应用是由下图组件结构所示,当我们要将蓝色节点的数据传递到最顶层,就要调用当前节点的父组件中的函数来间接传值(这里必须要具备react的基础知识才能看懂) ,然后父组件又继续调用爷爷组件中的函数继续传值,以此类推,最后将数值传递到根节点的组件上。这样一来组件传值就会变的非常麻烦,如何解决这个问题呢?这时我们就要引入数据层框架Redux来帮我解决复杂组件间的相互传值问题,由此可以证明React只是用来帮我们渲染DOM的,如果想开发复杂项目,仅仅用React框架是远远不够的。
Redux框架是如何帮我们解决组件间数据传递的呢?思路就是Redux将所有组件的数据都写入到Store公共空间中,当我们要将蓝色组件数据传递给各个组件的时候,我们把蓝色组件的数据放到这个公共的空间里(store),让后其它组件监听store空间里的数据,一旦发现数据改变了,就将数据取回到自己的组件中,这样就实现了组件间的数据传递。
Redux工作流程
如下图所示,乍一看这个工作流程很复杂,其实很简单,我会借助图书馆借书的流程来讲解Redux的工作流程。
蓝色框我们可以当作是借书的人,橙色框当作是图书管理员,紫色框当作是图书记录本,黄色框当作是我们对图书管理员说的话(比如我要借阅XXX书),它的工作流程是借书人(蓝色)说我要借xx书,它说的这句话就是黄色框(Action),管理员听到了要解什么书(橙色),但是他自己并不知道这本书到底在哪,所以要查看记录本(紫色),记录本把信息告诉管理员(橙色),最后管理员找到书并把书拿给借书人(蓝色)。
我再用React的数据流来表述一下Redux的工作流程,首先组件想更改数据,它通过Action(黄色)来告知Store要改变什么数据,但是Store本身并不清楚要改动数据的位置,所以要借助Reducers,Reducers把要更改数据告诉Store,Store将数据进行更新,最后把更新好的数据传递给组件。
Store创建
首先我们要创建一个新项目,成功创建好项目之后我们找到src目录,并创建一个ToDoList.js组件作为演示文件,代码如下:
import React,{Component} from "react";
class TodoList extends Component{
constructor(props){
super(props);
this.state={
list:["塞尔达传说-旷野之息","马里奥奥德赛","精灵宝可梦剑盾","异度之刃2"]
}
}
render(){
return (
{
this.state.list.map((item,inext)=>{
return {item}
})
}
)
}
}
export default TodoList;
index.js文件代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList.js';
ReactDOM.render(
,
document.getElementById('root')
);
页面渲染结果如下:
以上的数据全部放在了组件之中,如果我们想使用Redux来管理数据,就要先安装redux,先启动命令行工具,然后进入到项目中,输入如下命令:
cnpm install redux --save
安装完成之后,我们在src目录中创建一个名为store的文件夹,还是用图书馆的例子来介绍redux的使用,我们知道图书管理员(Store)并不知道图书馆里都放了哪些书还有书的位置,他要借助记录本来查看图书的信息。于是我们首先要创建这个记录本,也就是store文件下的reducer.js文件,代码如下:
let defaultState={
inputVal:"",
list:["塞尔达传说-旷野之息","马里奥奥德赛","精灵宝可梦剑盾","异度之刃"],
};
export default (state=defaultState,action)=>{
return state;
}
defaultState是这个图书馆所有书籍的存放的原始信息,state是记录本返回给图书管理员的图书信息,action之后再做介绍。
有了记录本,我们就可以创建store了(图书管理员),在store文件夹下创建index.js,代码如下:
import {createStore} from 'redux'
import reducer from './reducer'
const store=createStore(reducer);
export default store;
引入createStore来创建store,但是这个图书管理员并不知道图书的信息,所以我们要把记录本(reducer)传个这个store。现在管理员有了,记录本也有了,之后就可以向管理员借书了(获取数据)。
我们修改之前的TodoList.js代码如下:
import React,{Component} from "react";
import store from './store/index.js'
class TodoList extends Component{
constructor(props){
super(props);
this.state=store.getState();
}
render(){
return (
{
this.state.list.map((item,inext)=>{
return {item}
})
}
)
}
}
export default TodoList;
上述代码引入了store,store可以通过getState()方法获取图书信息(数据),然后渲染在页面上。
总结:先通过redux中的createStore创建store,然后创建一个reducer函数,并把这个函数作为参数传入到createStore中,最后通过store.getState()获取数据渲染在页面中。
Action和Reducer的编写
首先应该安装react-devtools调试工具,方便以后数据的查看,下载好了直接拖拽到谷歌浏览器的扩展窗口即可使用,这里要在store的index.js做一个配置,代码修改如下:
import {createStore} from 'redux'
import reducer from './reducer'
const store=createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
上一小结讲了组件如何从store中获取数据,这一小结主要介绍组件如何修改store里面的数据,这里还是要看之前的工作流程图,组件想要改变store里的数据,首先要创建一个Action,用这个Action来告知store我们要修改那些数据,store并不清楚要如何修改数据,它会把原有store里面的数据和Action转发给Reducer,Reducer会得到原始store里面的数据,并得知有哪个数据要改变,还有改变后数据的值,知道这些后,Reducer会将处理好的新数据返回给store(注意:reducer虽然能获取到store里面的数据,但是它不能直接将数据进行修改,只能对拷贝后的数据修改,最后返回给store),然后由store将原始数据替换成新的数据,这时虽然store里面的数据改变了,但是页面却不会更新,所以store给我们提供了一个subscribe方法,这个方法用于订阅store里面的数据,如果数据改变了,就会执行传入subscribe方法的参数(这个参数是一个函数),我们利用这个参数来重新渲染组件里面的数据。
下面将完善TodoList功能,我们将reducer.js做如下修改:
let defaultState={
inputVal:"",
list:["塞尔达传说-旷野之息","马里奥奥德赛","精灵宝可梦剑盾","异度之刃2"],
};
export default (state=defaultState,action)=>{
if(action.type==="input_change_value"){
let newState=JSON.parse(JSON.stringify(state));
newState.inputVal=action.value;
return newState;
}else if(action.type==="input_submit_value"){
let newState=JSON.parse(JSON.stringify(state));
newState.inputVal="";
newState.list.push(action.value);
return newState;
}else if(action.type==="input_delete_value"){
let newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.value,1);
return newState;
}
return state;
}
将TodoList.js做如下修改:
import React,{Component} from "react";
import store from './store/index.js'
class TodoList extends Component{
constructor(props){
super(props);
this.state=store.getState();
this.handleSubmit=this.handleSubmit.bind(this);
this.changeInput=this.changeInput.bind(this);
this.changeStore=this.changeStore.bind(this);
store.subscribe(this.changeStore);
}
render(){
return (
{
this.state.list.map((item,index)=>{
return {item}
})
}
)
}
changeInput(e){
let action={
type:"input_change_value",
value:e.target.value
}
store.dispatch(action);
}
handleSubmit(){
let value=this.state.inputVal;
let action={
type:"input_submit_value",
value
}
store.dispatch(action);
}
delItem(index){
let value=index;
let action={
type:"input_delete_value",
value
}
store.dispatch(action);
}
changeStore(){
this.setState(store.getState());
}
}
export default TodoList;
第一次写可能比较麻烦,但只要写了一个之后,基本上都是复制粘贴,大体思路就是按照流程图来写代码。
ActionType的拆分
TodoList.js里的aticon有一个type属性,这个属性是根reducer.js里的action type属性是相对应的,如果我们一不小心将type值拼写错误,控制台是不会报任何错误的,一旦发生这种情况,将给调试带来很大困难,很有可能找个一两个小时bug才发现原来是拼写错误,这样真的会很郁闷,所以要将actiont type进行拆分来进行优化。我们在store文件夹内创建actionTypes.js,代码如下:
export const INPUT_CHANGE_VALUE="input_change_value";
export const INPUT_SUBMIT_VALUE="input_submit_value";
export const INPUT_DELETE_VALUE="input_delete_value";
reducer.js代码修改如下:
import {INPUT_CHANGE_VALUE,INPUT_SUBMIT_VALUE,INPUT_DELETE_VALUE} from "./actionTypes.js"
let defaultState={
inputVal:"",
list:["塞尔达传说-旷野之息","马里奥奥德赛","精灵宝可梦剑盾","异度之刃2"],
};
export default (state=defaultState,action)=>{
if(action.type===INPUT_CHANGE_VALUE){
let newState=JSON.parse(JSON.stringify(state));
newState.inputVal=action.value;
return newState;
}else if(action.type===INPUT_SUBMIT_VALUE){
let newState=JSON.parse(JSON.stringify(state));
newState.inputVal="";
newState.list.push(action.value);
return newState;
}else if(action.type===INPUT_DELETE_VALUE){
let newState=JSON.parse(JSON.stringify(state));
newState.list.splice(action.value,1);
return newState;
}
return state;
}
TodeList.js代码修改如下:
import React,{Component} from "react";
import store from './store/index.js'
import {INPUT_CHANGE_VALUE,INPUT_SUBMIT_VALUE,INPUT_DELETE_VALUE} from "./store/actionTypes.js"
class TodoList extends Component{
constructor(props){
super(props);
this.state=store.getState();
this.handleSubmit=this.handleSubmit.bind(this);
this.changeInput=this.changeInput.bind(this);
this.changeStore=this.changeStore.bind(this);
store.subscribe(this.changeStore);
}
render(){
return (
{
this.state.list.map((item,index)=>{
return {item}
})
}
)
}
changeInput(e){
let action={
type:INPUT_CHANGE_VALUE,
value:e.target.value
}
store.dispatch(action);
}
handleSubmit(){
let value=this.state.inputVal;
let action={
type:INPUT_SUBMIT_VALUE,
value
}
store.dispatch(action);
}
delItem(index){
let value=index;
let action={
type:INPUT_DELETE_VALUE,
value
}
store.dispatch(action);
}
changeStore(){
this.setState(store.getState());
}
}
export default TodoList;
这样一来,如果type有拼写错误,控制台就会报错了。
使用actionCreator统一创建action
之前代码我们把action的创建都放在了组件中,写一些简单的项目确实可以这样做,但是如果是大型项目,就需要将创建action这个动作放入到一个文件进行统一的管理,这样代码的可维护性就比较高了,我们在store文件夹内创建actionCreators.js,代码如下:
import {INPUT_CHANGE_VALUE,INPUT_SUBMIT_VALUE,INPUT_DELETE_VALUE} from "./actionTypes.js"
export let getChangeValueAction=(value)=>{
return {
type:INPUT_CHANGE_VALUE,
value,
}
}
export let getSubmitValueAction=(value)=>{
return {
type:INPUT_SUBMIT_VALUE,
value,
}
}
export let getDeleteValueAction=(value)=>{
return {
type:INPUT_DELETE_VALUE,
value,
}
}
TodoList.js代码修改如下:
import React,{Component} from "react";
import store from './store/index.js'
import {getChangeValueAction,getSubmitValueAction,getDeleteValueAction} from "./store/actionCreators.js"
class TodoList extends Component{
constructor(props){
super(props);
this.state=store.getState();
this.handleSubmit=this.handleSubmit.bind(this);
this.changeInput=this.changeInput.bind(this);
this.changeStore=this.changeStore.bind(this);
store.subscribe(this.changeStore);
}
render(){
return (
{
this.state.list.map((item,index)=>{
return {item}
})
}
)
}
changeInput(e){
let action=getChangeValueAction(e.target.value);
store.dispatch(action);
}
handleSubmit(){
let action=getSubmitValueAction(this.state.inputVal);
store.dispatch(action);
}
delItem(index){
let action=getDeleteValueAction(index)
store.dispatch(action);
}
changeStore(){
this.setState(store.getState());
}
}
export default TodoList;
Redux的三要素
一、store唯一,整个应用只能有一个store存储空间。
二、只有store能改变自己的数据,Reducer只是将新的数据传递给store,然后由store进行数据的更新。
三、Reducer必须是纯函数,纯函数指的是给定固定的输入,就一定会有固定的输出,而且不会有副作用,比如一些异步操作或是和时间相关的操作都是不可以的,副作用指的是不可以直接对store数据进行修改。
以上就是Redux的最基础的内容讲解,之后如果有时间还会写一篇进阶内容,主要介绍下Redux的中间件的使用。
结束语
生活不会辜负一直向前走的人。
止水
于沈阳
2020/09/21 21:40