為什么要使用Redux
我們知道React是一個(gè)簡(jiǎn)單的視圖層框架,React開(kāi)發(fā)出的應(yīng)用是由許多組件構(gòu)建而成,如果我們開(kāi)發(fā)的應(yīng)用是由下圖組件結(jié)構(gòu)所示,當(dāng)我們要將藍(lán)色節(jié)點(diǎn)的數(shù)據(jù)傳遞到最頂層,就要調(diào)用當(dāng)前節(jié)點(diǎn)的父組件中的函數(shù)來(lái)間接傳值(這里必須要具備react的基礎(chǔ)知識(shí)才能看懂) ,然后父組件又繼續(xù)調(diào)用爺爺組件中的函數(shù)繼續(xù)傳值,以此類推,最后將數(shù)值傳遞到根節(jié)點(diǎn)的組件上。這樣一來(lái)組件傳值就會(huì)變的非常麻煩,如何解決這個(gè)問(wèn)題呢?這時(shí)我們就要引入數(shù)據(jù)層框架Redux來(lái)幫我解決復(fù)雜組件間的相互傳值問(wèn)題,由此可以證明React只是用來(lái)幫我們渲染DOM的,如果想開(kāi)發(fā)復(fù)雜項(xiàng)目,僅僅用React框架是遠(yuǎn)遠(yuǎn)不夠的。

Redux框架是如何幫我們解決組件間數(shù)據(jù)傳遞的呢?思路就是Redux將所有組件的數(shù)據(jù)都寫(xiě)入到Store公共空間中,當(dāng)我們要將藍(lán)色組件數(shù)據(jù)傳遞給各個(gè)組件的時(shí)候,我們把藍(lán)色組件的數(shù)據(jù)放到這個(gè)公共的空間里(store),讓后其它組件監(jiān)聽(tīng)store空間里的數(shù)據(jù),一旦發(fā)現(xiàn)數(shù)據(jù)改變了,就將數(shù)據(jù)取回到自己的組件中,這樣就實(shí)現(xiàn)了組件間的數(shù)據(jù)傳遞。
Redux工作流程
如下圖所示,乍一看這個(gè)工作流程很復(fù)雜,其實(shí)很簡(jiǎn)單,我會(huì)借助圖書(shū)館借書(shū)的流程來(lái)講解Redux的工作流程。

藍(lán)色框我們可以當(dāng)作是借書(shū)的人,橙色框當(dāng)作是圖書(shū)管理員,紫色框當(dāng)作是圖書(shū)記錄本,黃色框當(dāng)作是我們對(duì)圖書(shū)管理員說(shuō)的話(比如我要借閱XXX書(shū)),它的工作流程是借書(shū)人(藍(lán)色)說(shuō)我要借xx書(shū),它說(shuō)的這句話就是黃色框(Action),管理員聽(tīng)到了要解什么書(shū)(橙色),但是他自己并不知道這本書(shū)到底在哪,所以要查看記錄本(紫色),記錄本把信息告訴管理員(橙色),最后管理員找到書(shū)并把書(shū)拿給借書(shū)人(藍(lán)色)。
我再用React的數(shù)據(jù)流來(lái)表述一下Redux的工作流程,首先組件想更改數(shù)據(jù),它通過(guò)Action(黃色)來(lái)告知Store要改變什么數(shù)據(jù),但是Store本身并不清楚要改動(dòng)數(shù)據(jù)的位置,所以要借助Reducers,Reducers把要更改數(shù)據(jù)告訴Store,Store將數(shù)據(jù)進(jìn)行更新,最后把更新好的數(shù)據(jù)傳遞給組件。
Store創(chuàng)建
首先我們要?jiǎng)?chuàng)建一個(gè)新項(xiàng)目,成功創(chuàng)建好項(xiàng)目之后我們找到src目錄,并創(chuàng)建一個(gè)ToDoList.js組件作為演示文件,代碼如下:
import React,{Component} from "react";
class TodoList extends Component{
constructor(props){
super(props);
this.state={
list:["塞爾達(dá)傳說(shuō)-曠野之息","馬里奧奧德賽","精靈寶可夢(mèng)劍盾","異度之刃2"]
}
}
render(){
return (
<div>
<input value={this.state.inputVal } placeholder="to do item"/>
<button>提交</button>
{
this.state.list.map((item,inext)=>{
return <div>{item}</div>
})
}
</div>
)
}
}
export default TodoList;
index.js文件代碼如下:
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList.js';
ReactDOM.render(
<TodoList />,
document.getElementById('root')
);
頁(yè)面渲染結(jié)果如下:

以上的數(shù)據(jù)全部放在了組件之中,如果我們想使用Redux來(lái)管理數(shù)據(jù),就要先安裝redux,先啟動(dòng)命令行工具,然后進(jìn)入到項(xiàng)目中,輸入如下命令:
cnpm install redux --save
安裝完成之后,我們?cè)趕rc目錄中創(chuàng)建一個(gè)名為store的文件夾,還是用圖書(shū)館的例子來(lái)介紹redux的使用,我們知道圖書(shū)管理員(Store)并不知道圖書(shū)館里都放了哪些書(shū)還有書(shū)的位置,他要借助記錄本來(lái)查看圖書(shū)的信息。于是我們首先要?jiǎng)?chuàng)建這個(gè)記錄本,也就是store文件下的reducer.js文件,代碼如下:
let defaultState={
inputVal:"",
list:["塞爾達(dá)傳說(shuō)-曠野之息","馬里奧奧德賽","精靈寶可夢(mèng)劍盾","異度之刃"],
};
export default (state=defaultState,action)=>{
return state;
}
defaultState是這個(gè)圖書(shū)館所有書(shū)籍的存放的原始信息,state是記錄本返回給圖書(shū)管理員的圖書(shū)信息,action之后再做介紹。
有了記錄本,我們就可以創(chuàng)建store了(圖書(shū)管理員),在store文件夾下創(chuàng)建index.js,代碼如下:
import {createStore} from 'redux'
import reducer from './reducer'
const store=createStore(reducer);
export default store;
引入createStore來(lái)創(chuàng)建store,但是這個(gè)圖書(shū)管理員并不知道圖書(shū)的信息,所以我們要把記錄本(reducer)傳個(gè)這個(gè)store。現(xiàn)在管理員有了,記錄本也有了,之后就可以向管理員借書(shū)了(獲取數(shù)據(jù))。
我們修改之前的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 (
<div>
<input value={this.state.inputVal } placeholder="to do item"/>
<button>提交</button>
{
this.state.list.map((item,inext)=>{
return <div>{item}</div>
})
}
</div>
)
}
}
export default TodoList;
上述代碼引入了store,store可以通過(guò)getState()方法獲取圖書(shū)信息(數(shù)據(jù)),然后渲染在頁(yè)面上。
總結(jié):先通過(guò)redux中的createStore創(chuàng)建store,然后創(chuàng)建一個(gè)reducer函數(shù),并把這個(gè)函數(shù)作為參數(shù)傳入到createStore中,最后通過(guò)store.getState()獲取數(shù)據(jù)渲染在頁(yè)面中。
Action和Reducer的編寫(xiě)
首先應(yīng)該安裝react-devtools調(diào)試工具,方便以后數(shù)據(jù)的查看,下載好了直接拖拽到谷歌瀏覽器的擴(kuò)展窗口即可使用,這里要在store的index.js做一個(gè)配置,代碼修改如下:
import {createStore} from 'redux'
import reducer from './reducer'
const store=createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export default store;
上一小結(jié)講了組件如何從store中獲取數(shù)據(jù),這一小結(jié)主要介紹組件如何修改store里面的數(shù)據(jù),這里還是要看之前的工作流程圖,組件想要改變store里的數(shù)據(jù),首先要?jiǎng)?chuàng)建一個(gè)Action,用這個(gè)Action來(lái)告知store我們要修改那些數(shù)據(jù),store并不清楚要如何修改數(shù)據(jù),它會(huì)把原有store里面的數(shù)據(jù)和Action轉(zhuǎn)發(fā)給Reducer,Reducer會(huì)得到原始store里面的數(shù)據(jù),并得知有哪個(gè)數(shù)據(jù)要改變,還有改變后數(shù)據(jù)的值,知道這些后,Reducer會(huì)將處理好的新數(shù)據(jù)返回給store(注意:reducer雖然能獲取到store里面的數(shù)據(jù),但是它不能直接將數(shù)據(jù)進(jìn)行修改,只能對(duì)拷貝后的數(shù)據(jù)修改,最后返回給store),然后由store將原始數(shù)據(jù)替換成新的數(shù)據(jù),這時(shí)雖然store里面的數(shù)據(jù)改變了,但是頁(yè)面卻不會(huì)更新,所以store給我們提供了一個(gè)subscribe方法,這個(gè)方法用于訂閱store里面的數(shù)據(jù),如果數(shù)據(jù)改變了,就會(huì)執(zhí)行傳入subscribe方法的參數(shù)(這個(gè)參數(shù)是一個(gè)函數(shù)),我們利用這個(gè)參數(shù)來(lái)重新渲染組件里面的數(shù)據(jù)。
下面將完善TodoList功能,我們將reducer.js做如下修改:
let defaultState={
inputVal:"",
list:["塞爾達(dá)傳說(shuō)-曠野之息","馬里奧奧德賽","精靈寶可夢(mèng)劍盾","異度之刃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 (
<div>
<input value={this.state.inputVal } onChange={this.changeInput} placeholder="to do item"/>
<button onClick={this.handleSubmit}>提交</button>
{
this.state.list.map((item,index)=>{
return <div key={index} onClick={this.delItem.bind(this,index)}>{item}</div>
})
}
</div>
)
}
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;
第一次寫(xiě)可能比較麻煩,但只要寫(xiě)了一個(gè)之后,基本上都是復(fù)制粘貼,大體思路就是按照流程圖來(lái)寫(xiě)代碼。
ActionType的拆分
TodoList.js里的aticon有一個(gè)type屬性,這個(gè)屬性是根reducer.js里的action type屬性是相對(duì)應(yīng)的,如果我們一不小心將type值拼寫(xiě)錯(cuò)誤,控制臺(tái)是不會(huì)報(bào)任何錯(cuò)誤的,一旦發(fā)生這種情況,將給調(diào)試帶來(lái)很大困難,很有可能找個(gè)一兩個(gè)小時(shí)bug才發(fā)現(xiàn)原來(lái)是拼寫(xiě)錯(cuò)誤,這樣真的會(huì)很郁悶,所以要將actiont type進(jìn)行拆分來(lái)進(jìn)行優(yōu)化。我們?cè)趕tore文件夾內(nèi)創(chuàng)建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:["塞爾達(dá)傳說(shuō)-曠野之息","馬里奧奧德賽","精靈寶可夢(mèng)劍盾","異度之刃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 (
<div>
<input value={this.state.inputVal } onChange={this.changeInput} placeholder="to do item"/>
<button onClick={this.handleSubmit}>提交</button>
{
this.state.list.map((item,index)=>{
return <div key={index} onClick={this.delItem.bind(this,index)}>{item}</div>
})
}
</div>
)
}
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;
這樣一來(lái),如果type有拼寫(xiě)錯(cuò)誤,控制臺(tái)就會(huì)報(bào)錯(cuò)了。
使用actionCreator統(tǒng)一創(chuàng)建action
之前代碼我們把a(bǔ)ction的創(chuàng)建都放在了組件中,寫(xiě)一些簡(jiǎn)單的項(xiàng)目確實(shí)可以這樣做,但是如果是大型項(xiàng)目,就需要將創(chuàng)建action這個(gè)動(dòng)作放入到一個(gè)文件進(jìn)行統(tǒng)一的管理,這樣代碼的可維護(hù)性就比較高了,我們?cè)趕tore文件夾內(nèi)創(chuàng)建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 (
<div>
<input value={this.state.inputVal } onChange={this.changeInput} placeholder="to do item"/>
<button onClick={this.handleSubmit}>提交</button>
{
this.state.list.map((item,index)=>{
return <div key={index} onClick={this.delItem.bind(this,index)}>{item}</div>
})
}
</div>
)
}
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唯一,整個(gè)應(yīng)用只能有一個(gè)store存儲(chǔ)空間。
二、只有store能改變自己的數(shù)據(jù),Reducer只是將新的數(shù)據(jù)傳遞給store,然后由store進(jìn)行數(shù)據(jù)的更新。
三、Reducer必須是純函數(shù),純函數(shù)指的是給定固定的輸入,就一定會(huì)有固定的輸出,而且不會(huì)有副作用,比如一些異步操作或是和時(shí)間相關(guān)的操作都是不可以的,副作用指的是不可以直接對(duì)store數(shù)據(jù)進(jìn)行修改。
以上就是Redux的最基礎(chǔ)的內(nèi)容講解,之后如果有時(shí)間還會(huì)寫(xiě)一篇進(jìn)階內(nèi)容,主要介紹下Redux的中間件的使用。
結(jié)束語(yǔ)
生活不會(huì)辜負(fù)一直向前走的人。
止水
于沈陽(yáng)
2020/09/21 21:40