Redux使用指南
Redux使用指南
00-簡介
本文主要是用來記錄Redux結合React的使用過程,幫助大家在使用Redux的時候,能夠更好的理解Redux,從而更好地使用它
01-為什麼需要Redux
-
JavaScript的應用程序越來越複雜了,需要管理的狀態也越來越多了
這些狀態包括服務器返回的數據,緩存數據,用戶操作產生的數據,也包括一些UI的狀態
-
我們要管理不斷變化的狀態是非常困難的
狀態之間會互相依賴,一個狀態的變化會引起另一個狀態的變化,view頁面的變化也可能會引起狀態的變化
如果我們想要去追蹤一個狀態的變化是非常難的,因為我們不知道狀態在什麼時候發生了變化,因為什麼原因發生了變化
-
React在視圖層幫助我們解決了DOM的渲染過程,但是State依然是交給我們自己來進行管理的
例如組件自己定義的state,父子組件通過props傳遞信息,亦或是通過context進行全局共享狀態
React的核心思想
UI=render(state)
-
說到底,Redux是一個幫助我們管理狀態的容器,我們可以將需要管理的狀態放進這個容器中,這個容器給我們提供了可預測的狀態管理功能
02-Redux的核心理念
- store
- action
- reducer
-
Store:它是Redux提供給我們存儲所有狀態的容器,這也是我們尋找狀態的地方
-
action:它是一個JS對象類型的數據,是用來定義狀態的變化行為,主要由type和數據組成
const changeStateAction={type:"CHANGESTATE",state:state} const IncrementAction={type:"INCREMENT"}
可以看到 type是用來定義狀態發生改變的原因的,state就是你想要改變的狀態數據,這個state可加可不加,看自己的需求
這樣子使用action,我們可以很明確的知道,狀態改變的原因是什麼,方便我們跟蹤狀態和預測狀態
在Redux中,所有的action都是需要通過dispatch進行更新數據的,這一定請大家牢記
-
reducer:這是Redux關鍵的一個地方,它是負責將action和state聯繫起來的一個純函數
它可以將state和action結合起來生成一個新的state
03-Redux的三大原則
-
單一數據源
-
整個應用程序的state被存儲在一棵object tree中,並且這個Object Tree只存儲在一個Store中
-
單一數據源可以讓整個應用程序的state變得方便維護,追蹤和修改
-
-
State是只讀的
-
唯一修改state的途徑就是通過dispatch action,不要使用其他的方法去修改state
-
這樣子的好處就是 我們可以對狀態進行集中管理修改,並且會按照嚴格的順序執行,不需要擔心 race condition(競態)問題
-
這裡解釋一下競態的概念
競態:對於同樣的輸入,程序的輸出有時候正確而有時候卻是錯誤的。這種一個計算結果的正確性與時間有關的現象就被稱為競態(RaceCondition)
-
-
使用純函數來執行修改
只執行修改應該修改的狀態,不會修改其他地方的狀態,非常安全,修改狀態不用擔心會影響其他的地方
我來解釋一下純函數的概念
先來看下維基百科的定義
- 在程序設計中,若一個函數符合一下條件,那麼這個函數被稱為純函數:
- 此函數在相同的輸入值時,需產生相同的輸出。函數的輸出和輸入值以外的其他隱藏信息或狀態無關,也和由I/O設備產生的 外部輸出無關.
- 該函數不能有語義上可觀察的函數副作用,諸如「觸發事件」,使輸出設備輸出,或更改輸出值以外物件的內容等。 n 當然上面的定義會過於的晦澀,所以我簡單總結一下: p 確定的輸入,一定會產生確定的輸出; p 函數在執行過程中,不能產生副作用;
總結
- 確定的輸入有確定的輸出
- 在執行過程中,不會產生副作用.舉個例子,俗話說的好,是葯三分毒,吃藥雖然能治病,但是或多或少會影響我們身體的其他地方,這就是副作用.純函數就像吃藥不會對身體產生任何不好影響,100%有益,大家可以這麼理解這句話🌺
04-在項目中使用Redux
需要安裝以下包
- redux
- react-redux
- redux-thunk
- immutable
- redux-immutable
前三個包是肯定要安裝的,4,5你們可以自己選擇,4,5是用來對狀態存儲的結構進行一個優化的,可以對項目起到一個優化的作用
這些包我現在就不再說了,後面用到了再和你們細講
05-redux的目錄結構
- actionCreators:用來創建action的地方的
- constants:用來記錄action中的type常量的,統一管理
- reducer:負責將action和state聯繫起來
- index.js 容器創建的地方
06-redux的最基本使用
針對比較簡單的程序,你們可以只使用一個reducer,這裡也是講一個reducer的情況,後面會講到reducer拆分的情況
-
🍔創建一個store容器,定義存儲狀態的地方
import { createStore} from "redux" import reducer from './reducer.js'; const store = createStore(reducer); export default store;
-
🥓創建
constants
,定義好狀態改變的類型const ADDNUM="ADDNUM"; const SUBNUM="SUBNUM"; export { ADDNUM, SUBNUM, }
-
🍗創建actionCreators,定義好需要改變狀態的行為
import {ADDNUM,SUBNUM} from "./constants" //這裡寫成一個函數,是為了方便dispatch的時候,方便傳值的 //最後返回的是一個action對象 const addNumAction=(num)=>{ return { type:ADDNUM, //這裡其實可以直接寫num,而不用寫 num:num //這是js的一個特點,對象名和傳遞的參數相同,可以直接省略掉後面的參數的 num:num, } }; const subNumAction=(num)=>{ return { type:SUBNUM, num:num, } } export { addNumAction, subNumAction, }
-
🥗定義reducer,將action和state聯繫起來
import {ADDNUM,SUBNUM} from "./constants" //初始化state的值 const defaultValue={ num:0, } //reduce接收state和action兩個值 //state=defaultValue,是把默認值給state const reducer=(state=defaultValue,action)=>{ //根據action的type類型來判斷該執行什麼樣的操作 //沒有找打對應的類型就會返回state,不執行任何操作 switch(action.type){ case ADDSUM: return {...state,num:state.num+action.num} case SUBNUM: return {...state,num:state.num-action.num} default: return state } } export default reducer
-
在組件中使用
test.js
import React, { memo } from 'react' import { connect } from "react-redux" import { addNumAction, subNumAction, } from "../store2/actionCreators" //下面這步大家可能不理解 為啥可以拿到現在組件沒有的值 //實際上 我們是先讓這個組件拿到加強版組件的屬性 //因為加強後,這個原來組件的一切會被加強版組件繼承過去 到那個時候 就有這些屬性 //請結合我下面 connect函數詳解 這一節 來好好理解下 const Test = memo(function Test(props) { return ( <div> <h1>當前計數{props.num}</h1> <button onClick={e => props.addNum()}>加5</button> <button onClick={e => props.subNum()}>減5</button> </div> ) }) const mapStateToprops = state => { return { num: state.num } } const mapDispatchToprops = dispacth => { return { addNum: function () { dispacth(addNumAction(5)); }, subNum: function () { dispacth(subNumAction(5)); } } } export default connect(mapStateToprops, mapDispatchToprops)(Test);
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import store from './store2'; import { Provider } from 'react-redux'; import Test from './pages/test'; ReactDOM.render( <Provider store={store}> <Test /> </Provider>, document.getElementById('root') );
在組件中使用有幾個注意事項
- 在入口文件index.js中,首先從react-redux引入Provider,這是負責將我們創建的store容器,通過context共享出去,這裡注意一下,這個Provider是react-redux內部實現的,但本質上是context,最後會轉換為context
- 在test.js中有三個關鍵的地方
- mapStateToProps:負責將狀態映射到組件的屬性上
- mapDispatchToprops:負責將dispatch的action映射到屬性上,進行數據的更改
- connect:負責將1,2傳入到test中,形成一個新的組件,這裡用到了高階組件的方法,待會會細講
- 這幾個的核心思想是 將disptch的行為,和我們需要的狀態映射到組件上,這樣子組件能通過props拿到這些值和行為,從而自己進行相對應的操作和數據展示
07-connect函數詳解
我們這次來自己實現一下connect函數,看看react-redux究竟做了怎樣的一個操作
01-創建一個context
創建一個context文件,裏面放着我們的context
這一步的操作是模仿react-redux所提供的Provider,因為它的核心機制是context
import React from "react";
const StoreContext = React.createContext();
export { StoreContext };
02-在connect函數中進行一個引用
import React, { Component } from 'react'
import { StoreContext } from './context';
// 首先我們使用高階組件 來給我們的組件來傳遞函數
export default function connect(MapStateToProps, MapDispathToprop) {
// 返回值是一個函數 這個函數接收一個組件
// 這個函數的返回值是一個類組件 這個類組件是對之前傳進來的組件的一個加強版
return function Enhanced(WrappedComponent) {
return class EnhancedHOC extends Component {
static contextType = StoreContext;
constructor(props, context) {
// 因為context在構造器中,一開始是沒有值的,需要我們手動去給context賦值,從父類那繼承下context
//這樣就相當於把從contextProvider中傳遞過來的值拿到並給到構造器中的context
super(props, context);
//這一步是將容器中的state放入到 當前組件中的state中
this.state = {
storeState: MapStateToProps(context.getState()),
}
}
//需要更改的值 在生命周期函數componentDidMount()中完成
componentDidMount() {
// 訂閱store容器中的狀態變化 監聽到變化後,使用setState進行一個修改
// store.subscribe的返回值就是取消訂閱狀態變化
this.unSubscribe = this.context.subscribe(() => {
this.setState({
storeState: MapStateToProps(this.context.getState()),
})
})
}
componentWillUnmount() {
//在卸載組件的時候 將取消訂閱
this.unSubscribe();
}
render() {
return (
<WrappedComponent
{...this.props}
{...MapStateToProps(this.context.getState())}
{...MapDispathToprop(this.context.dispatch)} />
)
}
}
}
}
03-在入口文件中引入context即可
在這裡將store作為參數傳給context
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
// import { StoreContext } from './connect/context';
// 使用react-redux所需要引入的報
import { Provider } from 'react-redux';
import App from './App3';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));
// <StoreContext.Provider value={store}>
// <App />
// </StoreContext.Provider>
04-圖解
防止大家不明白,我畫了圖供大家理解
08-react-redux中Hook的使用和reducer的拆分
不知道大家在看完以上的介紹會不會覺得這樣用太麻煩了,用起來比較繁瑣
接下來我要給大家介紹下React在16.8版本新推出的Hook,有了Hook這個跨世紀的發明,React編寫起來也更加方便了
而React-redux也是大量用到了Hook這個新特性,結合著使用,比之前快了不知道多少倍
接下來 我舉得例子都是我寫的網易雲項目中 我是怎麼組織redux的,大家主要看思想即可,裏面一些我定義的變量不要過於計較了
主要是學習這個用法和思想
01-在根目錄下創建store文件夾
實際在寫項目的時候,各個頁面的狀態和行為是比較多的,這時候就需要我們去拆分reducer,讓每個頁面去管理自己的reducer,最後在這個主目錄下進行一個合併,一起放到store容器中
index.js
import { createStore, applyMiddleware} from "redux"
//為了在redux中進行異步請求,使用redux-thunk
import reduxThunkMiddleWare from "redux-thunk";
import zReducer from "./reducer";
//為了使用redux-thunk 需要從redux中引入applyMiddleware(中間件)
const storeEnhancer = applyMiddleware(reduxThunkMiddleWare);
// 在引用中間件後,在狀態容器中,你得使用composeEnhancers將應用的中間件進行一個包裹,再往裏面傳遞,在這個函數的定義內,第二個參數傳遞就是composeEnhancers
const store = createStore(zReducer, composeEnhancers(storeEnhancer));
export default store;
- 創建一個redux容器,容器是唯一的,雖然redux沒有規定必須一個,但是唯一的容器,方便我們追蹤狀態和查找
- 為了進行異步請求,使用redux-thunk,應用中間件
- 我怕還有小夥伴不知道該怎麼使用redux-thunk,我還是簡單的介紹下redux-thunk的使用步驟
- 下載redux-thunk這個包
- 引入redux-thunk,並從redux中引入applyMiddlware
- 將redux-thunk放入applyMiddleware
- 在創建容器的時候,用composeEnhancers對applyMiddleWare進行包裹傳入,其實這也是高階組件的用法,大家感興趣可以翻讀下源碼
reducer.js
// import { combineReducers } from 'redux';
import { combineReducers } from 'redux-immutable';
import { reducer as recommendReducer } from "../pages/discover/children_Pages/recommend/store";
import { reducer as songPlayer } from "../pages/player/store";
import { reducer as albumReducer } from "../pages/discover/children_Pages/album/store"
const zReducer = combineReducers({
recommend: recommendReducer,
player: songPlayer,
album: albumReducer,
})
export default zReducer
-
因為我在項目中使用的是immutable.js這個包,改變了我的數據存儲結構,所以是在redux-immutable引入的combineReducer中
-
這個combineReducer是用來幫助我們合併之前拆分的reducer的
-
combineReducer是傳入一個對象的
-
對象中的每個值代表着我們每個拆分的reducer
可能有小夥伴一頭霧水,請接着往下看,到後面所有東西就都會串聯起來
02-每個頁面有自己的Store
網頁應用有很多個頁面,我們拆分reducer的準則是每個頁面配備一個store
具體我們看實例 接下來的實例是音樂播放欄 我們看下目錄結構
大家看到這個結構是不是很熟悉呢,與我們之前講的部分的結構劃分是一摸一樣的
與之前不同的是 index.js文件中的內容變了,不再是創建容器了,而是負責將reducer導出去,然後在主目錄下的容器中引入合併reducer
reducer.js
//引入immutable 改變數據結構
import { Map } from "immutable"
import {
GETSONG, CURRENTSONGINDEX
} from "./constatnts"
//使用map對這個對象進行包裹
const defaultState = Map({
playList: [],
currentSongIndex: 0,
})
function reducer(state = defaultState, action) {
switch (action.type) {
case GETSONG:
return state.set("playList", action.playList);
case CURRENTSONGINDEX:
return state.set("currentSongIndex", action.currentSongIndex);
default:
return state;
}
}
export default reducer;
index.js
(大家可以看到 index沒有創建容器了,而是導出reducer了)
import reducer from "./reducer";
export {
reducer
};
actionCreators.js
//這是我們定義的網絡請求
import {
requestLyric,
} from "../../../service/player"
import * as actionType from "./constatnts"
const changeLyricAction = (lyric) => ({
type: actionType.GET_LYRIC,
lyric,
})
//因為引入了redux-thunk這個中間件,所以可以在redux中進行網絡請求
//我們需要請求數據的時候,使用這個行為進行派發即可
const getSongLyric = (id) => {
return (dispatch, getState) => {
const lyrics = getState().getIn(["player", "lyrics"]);
const songInfo = getState().getIn(["player", "songInfo"]);
const newLyrics = [...lyrics];
const songIndex = songInfo.findIndex(item => item.id === id);
requestLyric(id).then(res => {
if (songIndex === -1) {
newLyrics.push(res.data.lrc.lyric);
//拿到數據後 可以對我們定義的行為進行派發
dispatch(changeLyricAction(newLyrics));
} else {
dispatch(changeLyricAction(newLyrics));
}
})
}
}
export {getSongLyric}
03-組件中使用
import React, { memo, useEffect } from 'react'
import { shallowEqual, useSelector,useDispatch } from 'react-redux'
import {getSongLyric} from "./store/actionsCreators"
export default memo(function ZXPlayer() {
const dispatch=useDispatch();
const { lyrics, currentSongIndex } = useSelector(state => ({
//因為我們使用了immutable 所以我們取數據的時候得使用 getIn()
//shallowEqual是提升性能的一個方案,防止redux一直對我們的對象繼進行深層比較,消耗性能
lyrics: state.getIn(["player", "lyrics"]),
currentSongIndex: state.getIn(["player", "currentSongIndex"]),
}), shallowEqual);
useEffect(()=>{
dispatch(getSongLyric(id))
},[])
return <div>測試</div>
})
- 在組件中使用 我們引入了兩個Hook,這個是react-redux中所添加的 分別是useSelector和useDispatch
- useSelector:負責拿到我們存儲在容器中的狀態 對應的是mapStateToProps
- useDispatch:負責從容器中拿到dispatch這個行為 對應的是 mapDispatchToProps 不同的是這次沒有把行為添加進去,行為是由我們自己來控制派發的,hook只是幫助我們拿到了dispatch這個函數而已
- 我們在看了這個後,可以很明白的知道相比之前簡單了不是一點半點
- 以前我們需要自己定義state和需要派發的dispatch
- 現在我們只需要useSelector和useDispatch,就可以非常輕鬆的拿到state和dispatch,省去了一大筆麻煩
09-收尾
講的這,其實redux的使用就這麼多了,redux的使用難度我個人覺得並沒有很難,重點是去理解redux的工作原理是什麼,
connect函數是如何將state和dispatch組合到原來的組件上到,這個我覺得是重點,大家要好好理解connect函數的執行邏輯是什麼
文章後面講的Hook的使用,也只是幫助我們更簡單的使用redux而已,但是其核心是沒變的
好,這次我們就講的這吧,大家如果還有什麼不明白的歡迎留言,我看到就會回復大家的