React-Redux

138 阅读7分钟

一、Redux的核心思想

1. 为什么需要Redux

  • JavaScript开发的应用程序,已经变得越来越复杂了。JavaScript需要管理的状态越来越多,越来越复杂,这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动画,当前分野。
  • 管理不断变化的state是非常困难的。
  • React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理。无论是组件定义自己的state,还是组件之间的通信通过props进行传递,也包括通过Context进行数据之间的共享;React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定。
  • Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态姑那里。
  • Redux除了和React一起使用之外,它也可以和其他界面库一起来使用,并且它非常小。

2. Redux的核心理念

  • Store
    • store是一个仓库,用来存储数据,它可以获取数据,也可以派发数据,还能监听到数据的变化。
  • action
    • Redux要求我们通过action来更新数据;
    • 所有数据的变化,必须通过派发(dispatch)action来更新;
    • action是一个普通的JavaScript对象,用来描述这次的type和content。
  • reducer
    • reducer是一个纯函数;
    • reducer做的事情就是将传入的state和action结合起来生成一个新的state。

3. Redux的三大原则

  • 单一数据源
    • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个store中;
    • Redux并没有强制让我们不能创建多个Store,但是那样做不利于数据的维护;
    • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改。
  • state是只读的
    • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State;
    • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
    • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题。
  • 使用纯函数执行修改
    • 通过reducer将旧state和actions联系在一起,并且返回一个新的State;
    • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同;state tree的一部分
    • 但是所有的reducer都应该是纯函数,不能产生任何的副作用。

二、Redux基本使用

1. Redux的使用过程

  • 创建一个对象,作为我们要保存的状态;
  • 创建Store来存储这个state
    • 创建store时必须创建reducer
    • 我们可以通过store.getState来获取当前的state
  • 通过action来修改state
    • 通过dispatch来派发action
    • 通常action中都会有type属性,也可以携带其他的数据
  • 修改reducer中的处理代码
    • reducer是一个纯函数,不需要直接修改state
  • 可以在派发action之前,监听store的变化
 //redux代码
 //1. 将派发的action生成过程放到一个actionCreators函数中
 //2. 将定义的所有actionCreators的函数,放到一个独立的文件中:actionCreators.js
 //3. actionCreators和reducer函数中使用字符串常量是一致的,所以将常量抽取到一个独立constants的文件中
 //4. 将reducer和默认值(initialState)放到一个独立的reducer.js文件中,而不是在index.js

// --- store/index.js ---
const { createStore } = require("redux")
const reducer = require("./reducer.js")
// 创建的store
const store = createStore(reducer)
module.exports = store

// -- store/constants.js ---
// 常量
const ADD_NUMBER = "add_number"
const CHANGE_NAME = "change_num"

module.exports = {
  ADD_NUMBER,
  CHANGE_NAME
}

// --- store/reducer.js ---
const { CHANGE_NAME, ADD_NUMBER } = require("./constants")
// 初始化数据
const initialState = {
  name: "why",
  counter: 100
}
function reducer(state = initialState, action) {
  switch(action.type) {
    case CHANGE_NAME: 
      return { ...state, name: action.name }
    case ADD_NUMBER:
      return { ...state, counter: state.counter + action.num }
    default:
      return state
  }
}
module.exports = reducer

// --- store/actionCreators.js ---
const { CHANGE_NAME, ADD_NUMBER } = require("./constants")
const changNameAction = (name) => ({
  type: CHANGE_NAME,
  name
})
const addNumberAction = (num) => ({
  type: ADD_NUMBER,
  num
})
module.exports = {
  changNameAction,
  addNumberAction
}

// ---------
const store = require("./store")
const { addNumberAction, changNameAction } = require("./store/actionCreators")
//获取state
console.log(store.getState());
//通过action修改state
store.dispatch(changNameAction("kobe"))
//订阅state
const unsubscribe = store.subscribe(() => {
  console.log("订阅数据的变化:", store.getState());
})
//取消订阅
unsubscribe()

2. Redux的使用流程

06_Redux的使用流程.jpg

3. react-redux使用

  • redux和react没有直接的关系,但是redux是和React结合的更好
  • 安装react-redux
    • npm install react-redux
  • 代码
    // --- index.js ---
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    import { Provider } from "react-redux"
    import store from './store';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <React.StrictMode>
        <Provider store={store}>
          <App />
        </Provider>
      </React.StrictMode>
    );
    
    --- about.jsx ---
    import React, { PureComponent } from 'react'
    import { connect } from "react-redux"
    import { addNumberAction, subNumberAction } from '../store/actionCreator'
    
    export class about extends PureComponent {
      calcNumber(num, isAdd) {
        if (isAdd) {
          this.props.addNumber(num)
        } else {
          this.props.subNumber(num)
        }
      }
      render() {
        const { counter } = this.props
        return (
          <div>
            <h2>About Page: {counter}</h2>
            <div>
              <button onClick={e => this.calcNumber(6, true)}>+6</button>
            </div>
          </div>
        )
      }
    }
    
    // 将state映射到props中
    const mapStateToProps = (state) => ({
      counter: state.counter
    })
    // 将dispatch映射到props中
    const mapDispatchToProps = (dispatch) => ({
      addNumber: (num) => dispatch(addNumberAction(num)),
      subNumber: (num) => dispatch(subNumberAction(num))
    })
    // connect本身是一个高阶函数,接收两个函数作为参数,返回一个新的函数是高阶组件,接收一个组件作为参数
    export default connect(mapStateToProps, mapDispatchToProps)(about)
    

4. Redux的异步操作

  • Redux通过中间件来进行异步操作

  • 理解中间件

    • 中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一下自己的代码,比如日志记录、调用异步接口、添加代码调试功能等等
    • 推荐的中间件是redux-thunk
  • redux-thunk如何发送异步请求

    • 默认情况的dispatch(action),action需要是一个JavaScript的对象
    • redux-thunk可以让dispatch(action函数),action可以是一个函数
    • 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数
  • 如何使用redux

    • 安装redux-thunk:npm install redux-thunk
    • 在创建store时传入应用了middleware的enhance函数
      const enhancer = applyMiddleware(thunk)
      const store = createStore(reducer, enhancer)
      
    • 定义返回一个函数的action
      export const fetchHomeMultiDataAction = () => {
        return (dispatch, getState) => {
          axios.get("xxxxxx").then(res => {
            const banners = res.data.data.banner.list
            const recommends = res.data.data.recommend.list
            dispatch(changeBannersAction(banners))
            dispatch(changeRecommendsAction(recommends))
          })
        }
      }
      
  • reducer的模块划分

    • Redux提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并
    • 代码
      const reducer = combineReducers({
          counter: counterReducer,
          home: homeReducer
      })
      export default reducer
      

三、Redux Toolkit

1. 认识Redux Toolkit

  • Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题,也将之称为”RTK“
  • 安装Redux Toolkit
    • npm install @reduxjs/toolkit react-redux
  • Redux Toolkit的核心API
    • configureStore
      • 包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的slice reducer,添加你提供的任何Redux中间件,redux-thunk默认包含,并启用Redux DevTools Extension。
    • createSlice
      • 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。
    • createAsyncThunk
      • 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的thunk。

2. Redux Toolkit重构

  • 对reducer进行重构:通过createSlice创建一个slice
    • createSlice的参数
      • name:用来标记slice的名词
      • initialState:初始化值
      • reducers:两个参数(state/调用这个action时传递的action参数)
    • createSlice返回值是一个对象,包含所有的actions
  • store的创建:configureStore
    • configureStore的参数
      • reducer
      • middleware
      • devTools:是否配置devTools工具,默认为true
// --- features/counter.js ---
import { createSlice } from "@reduxjs/toolkit"

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    counter: 888
  },
  reducers: {
    addNumber(state, { payload }) {
      state.counter = state.counter + payload
    },
    subNumber(state, { payload }) {
      state.counter = state.counter - payload
    }
  }
})

export const { addNumber, subNumber } = counterSlice.actions
export default counterSlice.reducer

// --- index.js ---
import { configureStore } from "@reduxjs/toolkit"
import counterReducer from "./features/counter"

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

export default store

3. Redux Toolkit的异步操作

  • createAsyncThunk创建出来的action被dispatch时,会存在三种状态
    • pending:action被发出,但是还没有最终的结果
    • fulfilled:获取到最终的结果
    • rejected:执行过程中有错误或者抛出了异常
  • 我们可以在createSlice的entraReducer中监听这些结果
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from "axios"

export const fetchHomeMultidataAction = createAsyncThunk(
  "xxxxxx", 
  async (extraInfo, { dispatch, getState }) => {
    const res = await axios.get("xxxxxx")
    const banners = res.data.data.banner.list
    dispatch(changeBanners(banners))
    return res.data
})

const homeSlice = createSlice({
  name: "home",
  initialState: {
    banners: []
  },
  reducers: {
    changeBanners(state, { payload }) {
      state.banners = payload
    }
  },
  // 异步修改数据:监听异步函数的状态
  // extraReducers: {
    // [fetchHomeMultidataAction.pending](state, action) {
    //   console.log("fetchHomeMultidataAction pending");
    // },
    // [fetchHomeMultidataAction.fulfilled](state, { payload }) {
    //   console.log("fetchHomeMultidataAction fulfilled");
    //   state.banners = payload.data.banner.list
    // },
    // [fetchHomeMultidataAction.rejected](state, action) {
    //   console.log("fetchHomeMultidataAction rejected");
    // }
  // }
  // extraReducers的另一种写法:像builder中添加case来监听异步操作的结果
  extraReducers: (builder) => {
    builder.addCase(fetchHomeMultidataAction.pending, (state, action) => {
      console.log("fetchHomeMultidataAction pending");
    }).addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
      console.log("fetchHomeMultidataAction fulfilled");
      state.banners = payload.data.banner.list
      state.recommends = payload.data.recommend.list
    }).addCase(fetchHomeMultidataAction.rejected, (state, action) => {
      console.log("fetchHomeMultidataAction rejected");
    })
  }
})

export const { changeBanners, changeRecommends} = homeSlice.actions
export default homeSlice.reducer

4. 自定义connect函数

// --- utils/index.js ---
export { StoreContext } from "./StoreContext"
export { connect } from "./connect"

// --- utils/StoreContext.js ---
import { createContext } from "react";
export const StoreContext = createContext()

// --- utils/connect.js ---
// connect的参数
// 参数一:函数
// 参数二:函数
// 返回值:函数 => 高阶组件(接收一个组件,返回一个组件)
import { PureComponent } from "react";
import { StoreContext } from "./StoreContext";

export function connect(mapStateToProps, mapDispatchToProps) {
  return function(WrapperComponent) {
    class NewComponent extends PureComponent {
      constructor(props, context) {
        super(props)
        this.state = mapStateToProps(context.getState())
      }
      componentDidMount() {
        this.context.subscribe(() => {
          this.setState(mapStateToProps(this.context.getState()))
        })
      }
      render() {
        const stateObj = mapStateToProps(this.context.getState())
        const dispatchObj = mapDispatchToProps(this.context.dispatch)
        return <WrapperComponent {...this.props} {...stateObj} {...dispatchObj} />
      }
    }

    NewComponent.contextType = StoreContext
    
    return NewComponent
  }
}

// --- index.js ---
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from "react-redux"
import { StoreContext } from './hoc';
import App from './App';
import store from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
      <StoreContext.Provider value={store}>
        <App />
      </StoreContext.Provider>
    </Provider>
);

5. 状态管理方式

  • 方式一:组件自己的state管理
  • 方式二:Context数据的共享状态
  • 方式三:Redux管理应用状态