React Redux的使用详解(二)

403 阅读8分钟

一、ReduxToolKit使用

1.1 RTK介绍

  • Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

    • 在上一篇文章中,redux的编写逻辑过于的繁琐和麻烦, 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理)
    • 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

1.2 RTK基本使用

  • configureStore: 用于创建store对象

    • reducer,将slice中的reducer可以组成一个对象传入此处
    • middleware:可以使用参数,传入其他的中间件,默认集成redux-thunk
    • devTools:是否配置devTools工具,默认为true
    import { configureStore } from '@reduxjs/toolkit'
    
    import counterReducer from './features/counter'
    
    const store = configureStore({
      reducer: {
        counter: counterReducer
      },
      devTools: true
    })
    
    export default store
    
  • createSlice:

    • name:用户标记slice的名词,在之后的redux-devtool中会显示对应的名词;

    • initialState:初始化值

    • reducers:相当于之前的reducer函数

      • 对象类型,并且可以添加很多的函数

      • 函数类似于redux原来reducer中的一个case语句

      • 函数的参数:

        • 参数一:state

        • 参数二:调用这个action时,传递的action参数

    • createSlice 返回值是一个对象,包含所有的actions

    import { createSlice } from "@reduxjs/toolkit"
    
    const counterSlice = createSlice({
      name: 'counter',
      initialState: {
        counter: 555
      },
      reducers: {
        addNumber(state, action) {
          const payload = action.payload
          state.counter = state.counter + payload
        },
        subNumber(state, action) {
          const payload = action.payload
          state.counter = state.counter - payload
        }
      }
    })
    
    export const { addNumber, subNumber } = counterSlice.actions
    
    export default counterSlice.reducer
    
  • 在组件中使用

    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { addNumber } from '../store/features/counter'
    
    export  class Home extends PureComponent {
      addNumber(num) {
        this.props.addNumber(num)
      }
      render() {
        const { counter } = this.props
        return (
          <div>
            <h2>Home Counter: { counter }</h2>
            <button onClick={e => this.addNumber(5)}>+5</button>
            <button onClick={e => this.addNumber(8)}>+8</button>
          </div>
        )
      }
    }
    
    const mapStateToProps = state => ({ counter: state.counter.counter })
    const mapDispatchToProps = dispatch => ({
      addNumber(num) {
        dispatch(addNumber(num))
      }
    })
    
    export default connect(mapStateToProps, mapDispatchToProps)(Home)
    

1.3 RTK异步操作

  • Redux Toolkit 默认已经继承了Thunk相关的功能:createAsyncThunk

  • 当 createAsyncThunk 创建出来的action被dispatch时,会存在三种状态:

    • pending:action被发出,但是还没有最终的结果
    • fulfilled:获取到最终的结果(有返回值的结果)
    • rejected:执行过程中有错误或者抛出了异常
  • extraReducers:在createSlice的extraReducer中监听这些结果

    import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
    import axios from 'axios'
    
    export const fetchHomeMultidata = createAsyncThunk('fetch', async () => {
      const res = await axios.get('http://123.207.32.32:8000/home/multidata')
      return res.data
    })
    
    const homeSlice = createSlice({
      name: 'home',
      initialState: {
        banners: [],
        recommends: []
      },
      reducers: {
        changeBanners(state, { payload }) {
          state.banners = payload
        },
        changeRecommends(state, { payload }) {
          state.recommends = payload
        }
      },
      extraReducers: {
        [fetchHomeMultidata.pending](state, action) {
          console.log("fetchHomeMultidataAction pending")
        },
        [fetchHomeMultidata.fulfilled](state, { payload }) {
          console.log("fetchHomeMultidataAction fulfilled")
          state.banners = payload.data.banner.list
          state.recommends = payload.data.recommend.list
        },
        [fetchHomeMultidata.rejected](state, action) {
          console.log("fetchHomeMultidataAction rejected")
        }
      }
    })
    
    export const { changeBanners, changeRecommends } = homeSlice.actions
    
    export default homeSlice.reducer
    

1.4 RTK异步操作补充

  • extraReducers: (builder) => {builder.addCase()}

    import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
    import axios from 'axios'
    
    export const fetchHomeMultidata = createAsyncThunk('fetchHomeMultidata', async () => {
      const res = await axios.get('http://123.207.32.32:8000/home/multidata')
      return res.data
    })
    
    const homeSlice = createSlice({
      name: 'home',
      initialState: {
        banners: [],
        recommends: []
      },
      extraReducers: (builder) => {
        builder.addCase(fetchHomeMultidata.pending, (state, action) => {
          console.log("fetchHomeMultidataAction pending")
        }).addCase(fetchHomeMultidata.fulfilled, (state, { payload }) => {
          state.banners = payload.data.banner.list
          state.recommends = payload.data.recommend.list
        })
      }
    })
    
    export default homeSlice.reducer
    
  • createAsyncThunk("name", aysnc (extraInfo, store) => {})

    import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
    import axios from 'axios'
    
    export const fetchHomeMultidata = createAsyncThunk('fetchHomeMultidata', async (extraInfo, store) => {
      // 派发事件时可以额外传入的参数
      console.log(extraInfo);
      // 获取state中的数据
      console.log(store.getState())
    
      // 1.发送网络请求
      const res = await axios.get('http://123.207.32.32:8000/home/multidata')
    
      // 获取数据,并在此处直接dispatch
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      store.dispatch(changeBanners(banners))
      store.dispatch(changeRecommends(recommends))
    
      // 返回结果,那么action状态变成fulfilled
      return res.data
    })
    
    const homeSlice = createSlice({
      name: 'home',
      initialState: {
        banners: [],
        recommends: []
      },
      reducers: {
        changeBanners(state, { payload }) {
          state.banners = payload
        },
        changeRecommends(state, { payload }) {
          state.recommends = payload
        }
      },
    })
    
    export const { changeBanners, changeRecommends } = homeSlice.actions
    
    export default homeSlice.reducer
    

1.5 RTK的数据不可变性(底层原理)

  • 在React开发中,我们总是会强调数据的不可变性:

    • 无论是类组件中的state,还是redux中管理的state
    • 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的
  • 在前面经常会进行浅拷贝来完成某些操作,但是浅拷贝事实上也是存在问题的:

    • 比如过大的对象,进行浅拷贝也会造成性能的浪费
    • 比如浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响
  • 事实上,Redux Toolkit底层使用了 immerjs 的一个库来保证数据的不可变性

    • 这里有个ImmutableJS对象的概念:这个对象只要被修改就会返回一个新的对象,旧的对象不会发生改变

    • 为了节约内存,出现一个算法:Persistent Data Structure(持久化数据结构或一致性数据结构)

      • 用一种数据结构来保存数据
      • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费

二、connect底层原理

2.1 connect的实现

  • 分析

    • 传入两个参数,并且这两个参数都是函数
    • 返回一个函数,返回的这个函数是一个高阶组件
  • 实现

    // connect的参数
    // 参数一:函数
    // 参数二:函数
    // 返回值:函数 => 高阶组件
    
    import { PureComponent } from "react"
    import store from '../store'
    
    export default function connect(mapStateToProps, mapDispatchToProps) {
    
      // 高阶组件: 函数
      return function(WrapperComponent) {
    
        return class extends PureComponent {
          constructor(props) {
            super(props)
    
            // 将当前组件需要使用的状态放入state,监听变化时,执行setState
            this.state = mapStateToProps(store.getState())
          }
    
          componentDidMount() {
            this.unsubscribe = store.subscribe(() => {
              // 监听到数据变化,强制刷新,性能浪费
              // this.forceUpdate()
    
              this.setState(mapStateToProps(store.getState()))
            })  
          }
    
          componentWillUnmount() {
            this.unsubscribe()
          }
    
          render() {
            const stateObj = mapStateToProps(store.getState())
            const dispatchObj = mapDispatchToProps(store.dispatch)
    
            return <WrapperComponent { ...this.props } { ...stateObj } { ...dispatchObj } />
          }
        }
      }
    }
    

2.2 store的解耦操作

  • 上面实现的 connect 过于依赖于 store中的内容了,是一个很大的缺陷

  • 正确的做法是:提供一个 Provider,Provider来自于创建的Context,使用的时候直接将 store 传入到 value 中即可

  • 代码示例

    • 创建Context
    import { createContext } from "react"
    
    export const StoreContext = createContext()
    
    • 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'
    
    // 自己实现的connect
    import { StoreContext } from './hoc';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <React.StrictMode>
        <Provider store={store}>
          {/* 传入store */}
          <StoreContext.Provider value={store}>
            <App />
          </StoreContext.Provider>
        </Provider>
      </React.StrictMode>
    );
    
    • connect 使用 context
    import { PureComponent } from "react"
    // 使用context
    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.unsubscribe = this.context.subscribe(() => {
              this.setState(mapStateToProps(this.context.getState()))
            })  
          }
    
          componentWillUnmount() {
            this.unsubscribe()
          }
    
          render() {
    
            const stateObj = mapStateToProps(this.context.getState())
            const dispatchObj = mapDispatchToProps(this.context.dispatch)
    
            return <WrapperComponent { ...this.props } { ...stateObj } { ...dispatchObj } />
          }
        }
    
        NewComponent.contextType = StoreContext
    
        return NewComponent
      }
    }
    

三、Redux 核心原理

3.1 中间件原理

  • 中间件的目的:是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码

image.png

3.1.1 打印日志中间件的实现

  • 对每次派发的action进行拦截,打印日志

    // 对每次派发的action进行拦截
    function log(store) {
    
      // 保存最初的dispatch
      const next = store.dispatch
    
      function dispatchWithLog(action) {
        console.log('当前派发的action:', action)
        // 真正派发的代码: 使用之前的dispatch进行派发
        next(action)
        console.log('派发之后的结果', store.getState())
      }
    
      // monkey patch: 猴补丁 => 篡改现有的代码, 对整体的执行逻辑进行修改
      store.dispatch = dispatchWithLog
    }
    

3.1.2 thunk中间件的实现

  • 支持派发函数,两个参数:dispatch 和 getState

    function thunk(store) {
      const next = store.dispatch
    
      function dispatchWithFn(action) {
        if(typeof action === 'function') {
          // 传入最新的dispatch,因为可能在派发函数内部继续派发函数
          action(store.dispatch, store.getState)
        } else {
          next(action)
        }
      }
      store.dispatch = dispatchWithFn
    }
    

3.2 applyMiddleware简单实现

  • applyMiddleware实现

    // applyMiddleware.js
    function applyMiddleware(store, ...fns) {
      fns.forEach(fn => fn(store))
    }
    
    export default applyMiddleware
    
  • index.js 中使用

    // store/index.js
    import { createStore, combineReducers } from "redux"
    
    import { log, thunk, applyMiddleware } from "./middleware"
    
    import counterReducer from './counter'
    import homeReducer from './home'
    import userReducer from './user'
    
    // 将两个reducer合并到一起
    const reducer = combineReducers({
      counter: counterReducer,
      home: homeReducer,
      user: userReducer
    })
    
    const store = createStore(reducer)
    
    // 实现applyMiddleware
    applyMiddleware(store, log, thunk)
    
    export default store
    

四、React 状态管理选择

4.1 三种状态管理方式

  • 组件中自己的state管理

  • Context数据的共享状态

  • Redux管理应用状态

4.2 建议选择如下

  • UI相关的组件内部可以维护的状态,在组件内部自己来维护

  • 大部分需要共享的状态,都交给redux来管理和维护

  • 从服务器请求的数据(包括请求的操作),交给redux来维护

五、总结 Redux 的使用过程

5.1 原始 Redux

  • 先从react-redux中导入Providre包裹根组件

  • 将导出的store绑定到Provider组件的store属性中

  • 创建store:

    • actionCreator ---> 创建action对象
    • constant ---> 定义常量数据
    • reducer ---> 处理action对象,返回最新的state
    • index ---> 人口文件,创建store,使用中间件
      • const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
  • 组件中的使用方式

    • 定义函数 ---> mapStateToProps ---> 将store中的数据映射要组件的props中
    • 定义函数 ---> mapDispatchToProps ---> 将dispatch的操作映射到props中
    • 从react-redux中导入高阶组件对要导出的组件进行包裹,并把定义的函数传入connect函数
    • 组件触发相应的事件,dispatch相应的对象,store中的数据改变,组件重新渲染

5.2 RTK

  • 先从react-redux中导入Provider包裹根组件

  • 将导出的store绑定到Provider组件的store属性中

  • 创建store,目录结构:

    • index.js ---> 入口文件 ---> 创建和配置store ---> 主要是合并render
    • features ---> 要管理的数据模块
      • 使用createSliceAPI创建一个slice对象
      • name ---> 配置slice对象的名称
      • initialState ---> 定义初始值
      • reducer ---> 定义reduce函数的对象
      • 导出slice对象的actions---> 组件中使用或者自己内部使用,
      • 导出slice对象的reducer---> index文件合并reducer