前言
當(dāng)我們的前端項目功能點比較多歹苦,組件關(guān)系比較復(fù)雜時,單純的使用React原生的方法進(jìn)行組件數(shù)據(jù)的傳遞實現(xiàn)起來可能相對比較麻煩督怜,在這種場景下我們可能會需要有一個可以幫助我們做全局狀態(tài)管理的庫來解決這個問題殴瘦。
Redux
是一個專門用于做狀態(tài)管理的JS庫(不是react插件庫),他可以集中式管理React應(yīng)用中多個組件共享的狀態(tài)亮蛔。本篇文章將對redux
以及react-redux
的使用和功能進(jìn)行講解痴施,希望對各位讀者有所幫助。
一究流、先來了解Redux
(一)為什么要使用Redux
在講Redux的使用之前辣吃,我們不妨先回憶一下之前我們實現(xiàn)組件通信的方式都有哪些?
對于邏輯簡單的父子組件來說芬探,我們一般會直接使用props
參數(shù)來傳遞組件中的狀態(tài)或者方法神得,而對于兄弟組件的通信,我們可以先把數(shù)據(jù)交由兄弟組件共有的父組件偷仿,再通過props
參數(shù)傳遞到對應(yīng)的子組件哩簿。但這種做法較為麻煩,遇到嵌套層次比較深酝静,需要傳遞的屬性比較多時节榜,實現(xiàn)起來往往會大費周章。目前主流的解決思路主要有兩種别智,第一種是利用發(fā)布訂閱模式宗苍,組件間使用第三方庫進(jìn)行通信,常見的有庫有PubSubJS
薄榛,第二種解決思路就是將組件的狀態(tài)交由一個對象全局進(jìn)行管理讳窟,任何組件都可以通過這個全局對象來獲取其他組件的值。
Redux
就是第二種解決思路的解決方案敞恋,redux
是一個專門用于做狀態(tài)管理的JS庫(不是react插件庫)丽啡。它可以用在react
, angular
, vue
等項目中, 但基本與react
配合使用。
(二)Redux的學(xué)習(xí)文檔
Redux的學(xué)習(xí)資料還是蠻多的硬猫,一般來說我們可以去中文的官方文檔去查看對應(yīng)的API以及其他特性的使用:
- 英文文檔: https://redux.js.org/
- 中文文檔: http://www.redux.org.cn/
- Github: https://github.com/reactjs/redux
(三)Redux的工作過程
對于Redux的學(xué)習(xí)补箍,其實只要掌握了上面的工作流程圖,那么Redux的核心就掌握得差不多了啸蜜。原理圖中一共有4個對象馏予,ReactComponent指的是我們自定義的組件,而ActionCreators
盔性、Store
、Reducers
則是Redux中提出的新概念呢岗。
ActionCreator:用于創(chuàng)建action對象冕香,可以返回我們具體要對組件共享狀態(tài)進(jìn)行哪些操作蛹尝,比如說Redux幫助我們維護(hù)了A組件的count數(shù)據(jù),那么如果我們現(xiàn)在想要對count進(jìn)行+1的操作的話悉尾,我們就需要actionCreator幫助我們生成一個type
為add突那,data為1的對象。
Store:store是Redux中的核心构眯,負(fù)責(zé)管理state狀態(tài)和調(diào)度Reducer
愕难,當(dāng)我們把action交給store之后,store并不會直接對狀態(tài)進(jìn)行修改惫霸,而是交給對應(yīng)的reducer來對狀態(tài)進(jìn)行更新猫缭。
Reducer:reducer用于初始化狀態(tài)和加工狀態(tài),當(dāng)store指定reducer進(jìn)行更新狀態(tài)時壹店,reducer會根據(jù)原有的state和action進(jìn)行加工猜丹,返回新的state。
二硅卢、使用Redux來進(jìn)行狀態(tài)管理
(一)使用Redux來實現(xiàn)一個小需求
我們先來看這樣一個小案例:組件中有4個按鈕射窒,點擊后分別會對取下拉框的值對state中的值count進(jìn)行加
、減
将塑、奇數(shù)加
脉顿、異步加
的操作。
export default class Count extends Component {
state = {count:0}
//加法
increment = ()=>{
const {value} = this.selectNumber
const {count} = this.state
this.setState({count:count+value*1})
}
//減法
decrement = ()=>{
const {value} = this.selectNumber
const {count} = this.state
this.setState({count:count-value*1})
}
//奇數(shù)再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const {count} = this.state
if(count % 2 !== 0){
this.setState({count:count+value*1})
}
}
//異步加
incrementAsync = ()=>{
const {value} = this.selectNumber
const {count} = this.state
setTimeout(()=>{
this.setState({count:count+value*1})
},500)
}
render() {
return (
<div>
<h1>當(dāng)前求和為:{this.state.count}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>當(dāng)前求和為奇數(shù)再加</button>
<button onClick={this.incrementAsync}>異步加</button>
</div>
)
}
}
上面這種方式是通過純react的方式來實現(xiàn)的点寥,下面我們換成使用redux來實現(xiàn):
步驟一:安裝redux
npm install --save redux
步驟二:定義專門用于處理count狀態(tài)的reducer
關(guān)于這一步艾疟,需要對一個小細(xì)節(jié)做一下解釋,當(dāng)store分發(fā)對應(yīng)的動作給reducer時开财,reducer一共會收到2個參數(shù)棕孙,分別是preState和action炫掐,前者可以理解為當(dāng)前count的值,action即為即將對count進(jìn)行什么操作。但是在狀態(tài)初始化時弯院,此時由于count尚未存在,所以此時preState
就會為undefined
坞淮,而action將會是redux自動幫我們封裝好的一個action對象当纱,type為類似@@initxxxx
這樣的字符串,value為空恤溶。所以我們需要在countReducer中針對初始化這種情況乓诽,給count的初始值進(jìn)行賦值。
可以參考下面這種方式咒程,嫌麻煩的話也可以直接在default
中返回對應(yīng)的初始化值就行鸠天,比如這個案例就可以直接返回0。
const initState = 0 //初始化狀態(tài)
export default function countReducer(preState=initState,action){
// console.log(preState);
//從action對象中獲日室觥:type稠集、data
const {type,data} = action
//根據(jù)type決定如何加工數(shù)據(jù)
switch (type) {
case 'increment': //如果是加
return preState + data
case 'decrement': //若果是減
return preState - data
default:
return preState
}
}
步驟三:定義store.js
文件奶段,暴露store對象
這里的話,使用了redux的核心APIcreateStore
來創(chuàng)建store剥纷,傳入的參數(shù)是具體的reducer
import {createStore} from 'redux'
//引入為Count組件服務(wù)的reducer
import countReducer from './count_reducer'
//暴露store
export default createStore(countReducer)
步驟四:定義count狀態(tài)對應(yīng)的actionCreator
export const createIncrementAction = data => ({type: 'increment',data})
export const createDecrementAction = data => ({type: 'decrement',data})
步驟五:在Count組件中進(jìn)行共享狀態(tài)的獲取和修改
這里的話痹籍,我們使用了store.getState()
來獲取count當(dāng)前的狀態(tài),在具體修改狀態(tài)的方法中使用了store.dispatch()
來分發(fā)對應(yīng)的action晦鞋。
import React, { Component } from 'react'
//引入store蹲缠,用于獲取redux中保存狀態(tài)
import store from '../../redux/store'
//引入actionCreator,專門用于創(chuàng)建action對象
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
export default class Count extends Component {
state = {carName:'奔馳c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
store.dispatch(createIncrementAction(value*1))
}
//減法
decrement = ()=>{
const {value} = this.selectNumber
store.dispatch(createDecrementAction(value*1))
}
//奇數(shù)再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch(createIncrementAction(value*1))
}
}
//異步加
incrementAsync = ()=>{
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
render() {
return (
<div>
<h1>當(dāng)前求和為:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>當(dāng)前求和為奇數(shù)再加</button>
<button onClick={this.incrementAsync}>異步加</button>
</div>
)
}
}
通過上面這種方式悠垛,我們就可以實現(xiàn)把Count組件的count屬性交由Redux來進(jìn)行管理了线定。但此時還存在一個問題:Redux雖然已經(jīng)幫我們進(jìn)行了狀態(tài)的管理,可是當(dāng)狀態(tài)的值發(fā)生變更時鼎文,redux并不會幫我們刷新頁面渔肩。所以常見的我們有兩種方式來解決這個問題:
方式一:在組件的componentDidMount
鉤子中,定義刷新時機
下面的寫法表示拇惋,每當(dāng)store發(fā)生變更周偎,組件就會空調(diào)用一次setState()
,從而讓組件執(zhí)行render方法
componentDidMount(){
store.subscribe(()=>{
this.setState({})
})
}
方式二(常用):在最外層的index.js
中為組件綁定更新動作
由于App組件是所有組件的父組件撑帖,一旦store中的狀態(tài)發(fā)生更新蓉坎,
ReactDOM.render(<App/>,document.getElementById('root'))
store.subscribe(()=>{
ReactDOM.render(<App/>,document.getElementById('root'))
})
(二)引入constant.js
文件進(jìn)行優(yōu)化
上面的代碼雖然實現(xiàn)了Redux的狀態(tài)管理,但在實際使用中胡嘿,我們會對action中的type進(jìn)行統(tǒng)一的常量封裝蛉艾,最終維護(hù)到一個constant.js
文件中
步驟一:定義constant.js
文件
/*
該模塊是用于定義action對象中type類型的常量值,目的只有一個:便于管理的同時防止程序員單詞寫錯
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
步驟二:在count_reducer.js
中引入constant.js
/*
1.該文件是用于創(chuàng)建一個為Count組件服務(wù)的reducer衷敌,reducer的本質(zhì)就是一個函數(shù)
2.reducer函數(shù)會接到兩個參數(shù)勿侯,分別為:之前的狀態(tài)(preState),動作對象(action)
*/
import {INCREMENT,DECREMENT} from './constant'
const initState = 0 //初始化狀態(tài)
export default function countReducer(preState=initState,action){
// console.log(preState);
//從action對象中獲冉陕蕖:type助琐、data
const {type,data} = action
//根據(jù)type決定如何加工數(shù)據(jù)
switch (type) {
case INCREMENT: //如果是加
return preState + data
case DECREMENT: //若果是減
return preState - data
default:
return preState
}
}
步驟三:在count_action.js
文件中引入constant.js
/*
該文件專門為Count組件生成action對象
*/
import {INCREMENT,DECREMENT} from './constant'
//同步action,就是指action的值為Object類型的一般對象
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
//異步action面氓,就是指action的值為函數(shù),異步action中一般都會調(diào)用同步action兵钮,異步action不是必須要用的。
export const createIncrementAsyncAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data))
},time)
}
}
表面看抽取的constant.js
文件似乎反而增加了使用的復(fù)雜度舌界,但實際上當(dāng)變量比較多時掘譬,使用常量這種方式可以減少開發(fā)人員由于單詞拼寫錯誤導(dǎo)致的狀態(tài)更新失敗等問題。
三呻拌、異步action的定義
在之前的案例中葱轩,我們傳遞的action對象都是一般對象(plain Object)。但實際上,action還可以有另外一種類型: 異步action(其實也可以理解為是函數(shù)action)靴拱,也就是說我們對狀態(tài)的操作時放在異步的任務(wù)中完成复亏,同時,這個異步的任務(wù)不是組件自己來實現(xiàn)缭嫡,而是actionCreator來實現(xiàn)。需要注意的是
(1) 如果傳遞的action對象是函數(shù)抬闷,那么我們需要使用 redux-thunk 來對store進(jìn)行配置.
(2) 當(dāng)store調(diào)用dispatch方法的時候妇蛀,如果發(fā)現(xiàn)傳入的參數(shù)是函數(shù),那么store會幫我們調(diào)用這段函數(shù)笤成,并且傳遞給我們一個dispatch對象评架,供我們返回plain Object對象的時候直接調(diào)用。
上一小節(jié)的案例中炕泳,有一個按鈕的功能是異步加纵诞,異步的動作我們是在組件的方法中直接調(diào)用的,在本小節(jié)中培遵,我們將通過異步action來進(jìn)行實現(xiàn):
步驟一:引入redux-thunk
store默認(rèn)只支持一般對象浙芙,如果想要讓store支持函數(shù)的話,就需要借助redux-thunk
來進(jìn)行調(diào)和籽腕。
npm i redux-thunk
步驟二:在store.js
中引用redux-thunk
這里的話嗡呼,需要在redux中導(dǎo)入一個新的函數(shù):applyMiddleware,幫助store支持中間件
/**
* 這個js文件主要是用于對外暴露一個store對象
* store對象中需要傳入具體的reducer對象皇耗,reducer對象中定義了具體怎么操作數(shù)據(jù)的方法
*/
import {createStore, applyMiddleware} from 'redux'
import countReducer from './count_reducer'
// 引入 redux-thunk 用于支持異步action
import thunk from 'redux-thunk'
export default createStore(countReducer,applyMiddleware(thunk));
步驟三:在Count組件對應(yīng)的actionCreatoe中定義一個異步action
我們可以看到南窗,actionCreator一般的返回值是對象,我們這里的返回值是一個函數(shù)郎楼,默認(rèn)可以接收到dispatch
對象万伤,同時我們在函數(shù)中進(jìn)行了異步(設(shè)置定時器)的操作。
// 異步action
export const createAsyncIncrementAction = (data,time) => {
return (dispatch)=>{
setTimeout(()=>{
dispatch(createIncrementAction(data));
},time)
}
}
步驟四:在自定義組件中使用異步action
import React, { Component } from 'react';
import store from '../../redux/store';
import {createDecrementAction,createIncrementAction,createAsyncIncrementAction} from '../../redux/count_action'
export default class Count extends Component {
incrementAsync = () => {
const { value } = this.countNum;
store.dispatch(createAsyncIncrementAction(value*1,500));
}
render() {
return (
<div>
<h2>總數(shù)為: {store.getState()}</h2>
<select ref={a => this.countNum = a}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
...
<button onClick={this.incrementAsync}>add async</button>
</div>
)
}
}
我們可以看到呜袁,通過異步action我們就可以不需要自己在方法中手動寫異步函數(shù)了
四敌买、react-redux的使用
redux并不是react官方推出的狀態(tài)共享庫,但隨著使用人數(shù)的增加傅寡,react官方在后期也推出了**react-redux**來方便開發(fā)者更好的使用redux來進(jìn)行狀態(tài)的管理放妈。然后react-redux的使用和原先有些差別:
(1)提出了UI組件和容器組件的概念
(2)UI組件負(fù)責(zé)展示頁面和數(shù)據(jù),容器組件作為中間橋梁負(fù)責(zé)連接UI組件和redux之間的數(shù)據(jù)傳遞
react-redux雖然多了一些新概念荐操,相比于原先使用純粹的redux進(jìn)行狀態(tài)管理多了一定程度的復(fù)雜性芜抒,但是它也確實能夠簡化我們的一部分代碼。具體都簡化了哪些托启,我們不妨來看下面的實現(xiàn)步驟吧:
步驟一:下載react-redux
npm i react-redux
步驟二:定義容器組件
在已有UI組件的基礎(chǔ)上(這里我們以上一小節(jié)的組件作為UI組件)宅倒,我們新建一個和component
同級的container
目錄,里面存放我們的容器組件屯耸。創(chuàng)建容器組件主要依靠的是react-redux
庫中的connect
函數(shù)拐迁,connect
函數(shù)是一個高階函數(shù)(返回值還是一個函數(shù))蹭劈,第一次傳入的參數(shù)是兩個函數(shù),分別對應(yīng)傳遞給組件的狀態(tài)和可供組件調(diào)整狀態(tài)的方法线召,第二次傳入的參數(shù)就比較固定了铺韧,就是我們的UI組件。
import {connect} from 'react-redux'
import CountUI from '../../components/Count'
import {createIncrementAction,createDecrementAction,createAsyncIncrementAction} from '../../redux/count_action'
/**
* 由于 UI組件并不能直接獲取redux管理的狀態(tài)缓淹,所以這里的話需要由
* 容器組價將狀態(tài)以及操作狀態(tài)的方法作為參數(shù)傳遞到connect()函數(shù)中
*/
// 該方法返回的UI組件所需要獲取的redux管理的狀態(tài)屬性
function mapStateToProps(state){
return {count:state}
}
// 該方法返回的UI組件所需要的操作對應(yīng)狀態(tài)的方法
function mapDispatchToProps (dispatch){
return {
increment: number => dispatch(createIncrementAction(number)),
decrement: number => dispatch(createDecrementAction(number)),
incrementAsync: (number,time) => dispatch(createAsyncIncrementAction(number,time))
}
}
// 作為參數(shù)傳入后哈打,UI組件可以通過props屬性讀取對應(yīng)的值
export default connect(mapStateToProps,mapDispatchToProps)(CountUI);
當(dāng)然了,我們這里可以有更加簡潔的寫法:
(1)把UI組件和容器組件都放在同一個文件中
(2)把connect第一次調(diào)用的入?yún)⑦M(jìn)行格式的優(yōu)化讯壶,返回的action會由store自動幫助我們進(jìn)行分發(fā)料仗。
import { connect } from 'react-redux'
import { createIncrementAction, createDecrementAction, createAsyncIncrementAction } from '../../redux/count_action'
import React, { Component } from 'react';
class Count extends Component {
...
}
// 作為參數(shù)傳入后,UI組件可以通過props屬性讀取對應(yīng)的值
export default connect(
state => ({count: state }),
{
increment: createIncrementAction,
decrement: createDecrementAction,
incrementAsync: createAsyncIncrementAction
}
)(Count);
步驟三:調(diào)整UI組件中獲取共享狀態(tài)和更新狀態(tài)方法的方式
其實這里的話伏蚊,就是由原來的直接導(dǎo)入store
和actionCreator
來獲取數(shù)據(jù)改為通過props
進(jìn)行獲取立轧。
class Count extends Component {
// 有了actionCreator , 我們就不需要自己再去定義action了
increment = () => {
const { value } = this.countNum;
this.props.increment(value * 1)
}
decrement = () => {
const { value } = this.countNum;
this.props.decrement(value * 1)
}
incrementIfOdd = () => {
const { value } = this.countNum;
if (this.props.count % 2 === 1) {
this.props.increment(value * 1)
}
}
incrementAsync = () => {
const { value } = this.countNum;
this.props.incrementAsync(value * 1, 500)
}
render() {
return (
<div>
<h2>總數(shù)為: {this.props.count}</h2>
...
</div>
)
}
}
步驟四:在App組件中給Count組件傳入store對象
組件有了store
對象躏吊,容器組件才可以獲取到store中的state
以及dispatch
對象
import React,{Component} from 'react';
import Count from './containers/Count';
import store from './redux/store'
export default class App extends Component{
render(){
return (
<div>
<Count store={store}/>
</div>)
}
}
容器組件一旦數(shù)量多了氛改,我們可能就不得不每個組件都要手動傳遞store
屬性進(jìn)去,這樣比較麻煩颜阐,我們可以使用react-redux
中自帶的<Provider>
組件來幫助我們簡化這一步的操作平窘。具體的步驟如下:
(1)在最外層的index.js中使用<Provider>
標(biāo)簽來包裹<App/>
標(biāo)簽
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {Provider} from 'react-redux'
import store from './redux/store'
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,document.getElementById('root'));
(2)原有的App.js
文件可以不用傳遞store屬性了
import React,{Component} from 'react';
import Count from './containers/Count';
export default class App extends Component{
render(){
return (
<div>
<Count/>
</div>)
}
}
在這里,我們對使用react-redux
進(jìn)行開發(fā)的優(yōu)勢和注意事項來做一個小結(jié):
(1)react-redux
有著UI組件和容器組件的概念凳怨,UI組件并不能直接和redux進(jìn)行溝通瑰艘,而是要借助容器組件作為中間橋梁,獲取和操作組件共享狀態(tài)的方法都由容器組件來提供肤舞。這樣雖然一定程度上增加了使用的復(fù)雜性紫新,但是讓組件的職責(zé)變得更加清晰。
(2)使用react-redux
可以自動實現(xiàn)狀態(tài)和頁面的聯(lián)動刷新李剖,讓我們不需要給APP組件或者其他自定義組件進(jìn)行store.subscribe()
的動作更新綁定了芒率。
(3)針對react-redux有專門的開發(fā)者工具(瀏覽器插件)可以使用,幫我們更好地分析組件的狀態(tài)篙顺。
五偶芍、store中存在多個共享狀態(tài)的處理
在之前的案例中,無論是使用redux
還是react-redux
德玫,我們都只是對store只保存一個狀態(tài)的場景進(jìn)行演示匪蟀,但實際中稍微大一些的項目,基本上store中都是會存放多個狀態(tài)值的(否則使用也就失去了使用redux的意義)宰僧。
實際上材彪,當(dāng)store只存儲一個數(shù)值時,此時 store.state
的值就是一個number類型的數(shù)字,要想滿足store可以存放多個數(shù)量段化、多種數(shù)據(jù)類型的狀態(tài)嘁捷,那么此時的store.state
就需要是Object類型的才可以滿足。
假設(shè)我們現(xiàn)在在原有案例的基礎(chǔ)上新增了一個Person組件显熏,那么此時的store又應(yīng)該做出什么調(diào)整呢雄嚣?
步驟一:使用combineReducers
函數(shù), 整合多個reducer喘蟆,再作為參數(shù)傳遞給store
import {createStore, applyMiddleware,combineReducers} from 'redux';
// 引入 redux-thunk 用于支持異步action
import thunk from 'redux-thunk';
import countReducer from './reducers/count';
import personReducer from './reducers/person';
// 注意现诀,如果是有多個狀態(tài)需要保存,那么在一開始調(diào)用combineReducers的時候履肃,就要設(shè)置好對象中各個狀態(tài)的key
const allReducers = combineReducers({
count:countReducer,
persons:personReducer
})
export default createStore(allReducers,applyMiddleware(thunk));
步驟二:調(diào)整對應(yīng)容器組件獲取狀態(tài)的方式
比如之前的Count組件就不再直接通過count = state
的方式來獲取狀態(tài)了,而是通過count = state.count
來獲取坐桩。
export default connect(
state => ({count: state.count }),
{
increment: createIncrementAction,
decrement: createDecrementAction,
incrementAsync: createAsyncIncrementAction
}
)(Count);
需要注意的是尺棋,在實際開發(fā)中,為了讓store.js
文件更加清晰绵跷,我們常常會將各個狀態(tài)的reducer
單獨抽取出來到一個文件中進(jìn)行整合膘螟,再導(dǎo)入到store.js
文件中。
說在最后:
Redux雖然可以幫助我們更好地管理組件間共享的狀態(tài)碾局,但redux除了需要額外的學(xué)習(xí)成本之外荆残,也一定程度上增加了項目的復(fù)雜性。如果只是小型項目净当,一般建議還是不引入Redux内斯,直接使用原生react特性可能更為方便。
而react-redux
其實只是官方為了方便我們更好地使用redux而推出的一個集成庫像啼,實際上可用可不用俘闯,只是說react-redux
在某些方面也確實起到了簡化部分開發(fā)工作的作用。所以在實際應(yīng)用中忽冻,大家可以根據(jù)實際情況來使用真朗。
本篇文章的詳細(xì)案例代碼,可以在我的碼云上面下載:https://gitee.com/moutory/redux_test