Redux使用指南

Redux使用指南

00-簡介

本文主要是用來記錄Redux結合React的使用過程,幫助大家在使用Redux的時候,能夠更好的理解Redux,從而更好地使用它

01-為什麼需要Redux

  1. JavaScript的應用程序越來越複雜了,需要管理的狀態也越來越多了

    這些狀態包括服務器返回的數據,緩存數據,用戶操作產生的數據,也包括一些UI的狀態

  2. 我們要管理不斷變化的狀態是非常困難的

    狀態之間會互相依賴,一個狀態的變化會引起另一個狀態的變化,view頁面的變化也可能會引起狀態的變化

    如果我們想要去追蹤一個狀態的變化是非常難的,因為我們不知道狀態在什麼時候發生了變化,因為什麼原因發生了變化

  3. React在視圖層幫助我們解決了DOM的渲染過程,但是State依然是交給我們自己來進行管理的

    例如組件自己定義的state,父子組件通過props傳遞信息,亦或是通過context進行全局共享狀態

    React的核心思想UI=render(state)

  4. 說到底,Redux是一個幫助我們管理狀態的容器,我們可以將需要管理的狀態放進這個容器中,這個容器給我們提供了可預測的狀態管理功能

02-Redux的核心理念

  1. store
  2. action
  3. 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的三大原則

  1. 單一數據源

    • 整個應用程序的state被存儲在一棵object tree中,並且這個Object Tree只存儲在一個Store中

    • 單一數據源可以讓整個應用程序的state變得方便維護,追蹤和修改

  2. State是只讀的

    • 唯一修改state的途徑就是通過dispatch action,不要使用其他的方法去修改state

    • 這樣子的好處就是 我們可以對狀態進行集中管理修改,並且會按照嚴格的順序執行,不需要擔心 race condition(競態)問題

    • 這裡解釋一下競態的概念

      競態:對於同樣的輸入,程序的輸出有時候正確而有時候卻是錯誤的。這種一個計算結果的正確性與時間有關的現象就被稱為競態(RaceCondition)

  3. 使用純函數來執行修改

    只執行修改應該修改的狀態,不會修改其他地方的狀態,非常安全,修改狀態不用擔心會影響其他的地方

    我來解釋一下純函數的概念

    先來看下維基百科的定義

    • 在程序設計中,若一個函數符合一下條件,那麼這個函數被稱為純函數:
    1. 此函數在相同的輸入值時,需產生相同的輸出。函數的輸出和輸入值以外的其他隱藏信息或狀態無關,也和由I/O設備產生的 外部輸出無關.
    2. 該函數不能有語義上可觀察的函數副作用,諸如「觸發事件」,使輸出設備輸出,或更改輸出值以外物件的內容等。 n 當然上面的定義會過於的晦澀,所以我簡單總結一下: p 確定的輸入,一定會產生確定的輸出; p 函數在執行過程中,不能產生副作用;

    總結

    1. 確定的輸入有確定的輸出
    2. 在執行過程中,不會產生副作用.舉個例子,俗話說的好,是葯三分毒,吃藥雖然能治病,但是或多或少會影響我們身體的其他地方,這就是副作用.純函數就像吃藥不會對身體產生任何不好影響,100%有益,大家可以這麼理解這句話🌺

04-在項目中使用Redux

需要安裝以下包

  1. redux
  2. react-redux
  3. redux-thunk
  4. immutable
  5. redux-immutable

前三個包是肯定要安裝的,4,5你們可以自己選擇,4,5是用來對狀態存儲的結構進行一個優化的,可以對項目起到一個優化的作用

這些包我現在就不再說了,後面用到了再和你們細講

05-redux的目錄結構

image-20211004153642148

  1. actionCreators:用來創建action的地方的
  2. constants:用來記錄action中的type常量的,統一管理
  3. reducer:負責將action和state聯繫起來
  4. index.js 容器創建的地方

06-redux的最基本使用

針對比較簡單的程序,你們可以只使用一個reducer,這裡也是講一個reducer的情況,後面會講到reducer拆分的情況

  1. 🍔創建一個store容器,定義存儲狀態的地方

    import { createStore} from "redux"
    import reducer from './reducer.js';
    const store = createStore(reducer);
    export default store;
    
  2. 🥓創建constants,定義好狀態改變的類型

    const ADDNUM="ADDNUM";
    const SUBNUM="SUBNUM";
    export {
    	ADDNUM,
        SUBNUM,
    }
    
  3. 🍗創建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,
    }
    
  4. 🥗定義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
    
  5. 在組件中使用

    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')
    );
    

    在組件中使用有幾個注意事項

    1. 在入口文件index.js中,首先從react-redux引入Provider,這是負責將我們創建的store容器,通過context共享出去,這裡注意一下,這個Provider是react-redux內部實現的,但本質上是context,最後會轉換為context
    2. 在test.js中有三個關鍵的地方
      1. mapStateToProps:負責將狀態映射到組件的屬性上
      2. mapDispatchToprops:負責將dispatch的action映射到屬性上,進行數據的更改
      3. connect:負責將1,2傳入到test中,形成一個新的組件,這裡用到了高階組件的方法,待會會細講
      4. 這幾個的核心思想是 將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-圖解

防止大家不明白,我畫了圖供大家理解

image-20211004180808072

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;
  1. 創建一個redux容器,容器是唯一的,雖然redux沒有規定必須一個,但是唯一的容器,方便我們追蹤狀態和查找
  2. 為了進行異步請求,使用redux-thunk,應用中間件
  3. 我怕還有小夥伴不知道該怎麼使用redux-thunk,我還是簡單的介紹下redux-thunk的使用步驟
    1. 下載redux-thunk這個包
    2. 引入redux-thunk,並從redux中引入applyMiddlware
    3. 將redux-thunk放入applyMiddleware
    4. 在創建容器的時候,用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
  1. 因為我在項目中使用的是immutable.js這個包,改變了我的數據存儲結構,所以是在redux-immutable引入的combineReducer中

  2. 這個combineReducer是用來幫助我們合併之前拆分的reducer的

  3. combineReducer是傳入一個對象的

  4. 對象中的每個值代表着我們每個拆分的reducer

可能有小夥伴一頭霧水,請接着往下看,到後面所有東西就都會串聯起來


02-每個頁面有自己的Store

網頁應用有很多個頁面,我們拆分reducer的準則是每個頁面配備一個store

具體我們看實例 接下來的實例是音樂播放欄 我們看下目錄結構

image-20211005174001233

大家看到這個結構是不是很熟悉呢,與我們之前講的部分的結構劃分是一摸一樣的

與之前不同的是 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>
})

  1. 在組件中使用 我們引入了兩個Hook,這個是react-redux中所添加的 分別是useSelector和useDispatch
  2. useSelector:負責拿到我們存儲在容器中的狀態 對應的是mapStateToProps
  3. useDispatch:負責從容器中拿到dispatch這個行為 對應的是 mapDispatchToProps 不同的是這次沒有把行為添加進去,行為是由我們自己來控制派發的,hook只是幫助我們拿到了dispatch這個函數而已
  4. 我們在看了這個後,可以很明白的知道相比之前簡單了不是一點半點
  5. 以前我們需要自己定義state和需要派發的dispatch
  6. 現在我們只需要useSelector和useDispatch,就可以非常輕鬆的拿到state和dispatch,省去了一大筆麻煩

09-收尾

講的這,其實redux的使用就這麼多了,redux的使用難度我個人覺得並沒有很難,重點是去理解redux的工作原理是什麼,

connect函數是如何將state和dispatch組合到原來的組件上到,這個我覺得是重點,大家要好好理解connect函數的執行邏輯是什麼

文章後面講的Hook的使用,也只是幫助我們更簡單的使用redux而已,但是其核心是沒變的

好,這次我們就講的這吧,大家如果還有什麼不明白的歡迎留言,我看到就會回復大家的

Tags: