彻底理解Redux的使用

理解纯函数

函数式编程中有一个非常重要的概念叫纯函数,在react开发中纯函数是被多次提及的,所以掌握纯函数对于理解很多框架的设计是非常有帮助的

  • 若一个函数符合以下条件,那么这个函数被称为纯函数:

    • 此函数在相同的输入值时,需产生相同的输出

    • 函数的输出和输入值以外的其他隐藏信息或状态无关 ,也和由I/O设备产生的外部输出无关

    • 函数在执行过程中不能产生副作用,诸如触发事件,使输出设备输出,或更改输出值以外物件的内容等

      副作用的理解:表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量、修改参数或者改变外部的存储

  • 案例:

    • sliceslice截取数组时不会对原数组进行任何操作,而是生成一个新的数组,slice就是一个纯函数

    • React中就要求无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改

    • 在接下来学习redux中,reducer也被要求是一个纯函数

为什么需要Redux

  • JavaScript开发的应用程序变得越来越复杂了,需要管理的状态越来越多

  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示 加载动效,当前分页

  • 管理不断变化的state是非常困难的 ,状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化

  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪

  • React是在视图层帮助解决了DOM的渲染过程,但是State依然是留给自己来管理 ,无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享

  • Redux就是一个帮助管理State的容器ReduxJavaScript的状态容器,提供了可预测的状态管理

  • Redux除了和React一起使用之外,它也可以和其他界面库一起来使用 (比如Vue),并且它非常小(包括依赖在内,只有2kb

核心理念

Redux 的核心概念围绕状态管理展开,旨在使应用的状态变化可预测和易于调试

store

State 是应用的全部数据,存储在一个单一的 JavaScript 对象中

  • 特点只读不能直接修改 ,通过 actionreducer 更新

  • 示例:

    js 复制代码
    const initialState = {
      user: { name: 'Alice', age: 25 },
      todos: [{ id: 1, text: 'Learn Redux', completed: false }]
    }

action

Action 是一个普通的 JavaScript 对象,用于描述发生了什么(这次更新的typecontent

  • 它是更新 State 的唯一来源 ,所有数据的变化,必须通过派发(dispatchaction来更新

  • 结构 :必须包含一个 type 字段表示 action 的类型,可以包含其他字段(如 payload)传递数据

    js 复制代码
    const action = {
      type: 'ADD_TODO',
      payload: { id: 2, text: 'Learn React', completed: false }
    }
  • 上面的action是固定的对象,真实应用中会通过函数来定义并导出

    js 复制代码
    const changeName1Action = ((name)=> ({type: 'change_name', name}))
  • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟踪、可预测的

reducer

如何将stateaction联系在一起呢?答案就是reducerReducer 是一个纯函数,接收当前 StateAction,返回新的 State

  • 特点

    • 必须是纯函数(相同的输入始终返回相同的输出,无副作用)

    • 不能直接修改 State,而是返回一个新的 State 对象

  • 示例:

    js 复制代码
    function todosReducer(state = initialState, action) {
      switch (action.type) {
        case 'ADD_TODO':
          return [...state, action.payload];
        default:
          return state;
      }
    }

三大原则

这三大原则确保了 Redux 的可预测性、可调试性和维护性

  • 单一数据源:让整个应用的状态集中管理,避免了多处状态的不一致

    • Redux 的状态树(state tree)是单一的,整个应用的状态都存储在一个对象树中,这个状态树就是整个应用的单一数据源

    • Redux并没有强制不能创建多个Store,但是那样做并不利于数据的维护

    • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改

    • 例如应用的所有UI、用户信息、设置等都在一个全局的 state 中维护

  • 状态是只读的:防止直接修改状态,确保所有的状态变化都通过标准化的流程进行

    • Redux 中的 state 不能直接修改,而是只能通过派发(dispatch)行为来间接修改

    • 修改 state 的唯一途径是触发一个action,并通过一个 reducer 来计算新的 state

    • 这确保了状态更新的可追溯性,并避免了由于直接修改 state 而引发的难以调试的错误

  • 改变状态只通过纯函数:通过纯函数来避免副作用,确保状态变化是可预测的

    • 通过 reducer 函数来改变 statereducer 是一个纯函数,即给定相同的输入它总是返回相同的输出,并且不会修改输入参数

    • 每次 state 更新时,reducer 会根据当前 stateaction 来返回一个新的 state 对象,而不是直接修改现有的 state

    • 这也意味着 Redux 中的 state 是不可变的(immutable),避免了由于直接修改数据而产生的副作用

    • 随着应用程序的复杂度增加,可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分

单独使用

redux的基本使用之前先完成下面的准备工作:

  • 创建一个新的项目文件夹learn_redux,执行npm init

  • 安装reduxnpm install redux --save

  • 创建src目录 ,并且创建index.js文件

  • 修改package.json可以执行index.js"scripts": { "start": "node src/index.js" }

基本

  • 创建对象初始化数据const initialState = {name: "shine", counter: 100}

  • 创建renducer纯函数不直接修改statefunction renducer(state = initialState, action) {// 写根据action修改state逻辑}

  • 创建Store来存储这个stateconst store = createStore(reducer, enhancer)

    • reducer:一个纯函数,用于定义如何更新 state

    • enhancer(可选):用于增强 store 的功能,例如添加中间件或开发者工具,后面学习

  • 可以通过 store.getState 来获取当前的state

  • 可以在派发action之前,监听store的变化const unsubscribe = store.subscribe(()=> {console.log("订阅数据的变化:", store.getState())})

  • 可以通过dispatch来派发action修改statestore.dispatch({type: 'change_name', name: 'changeName2'})

  • 可以取消订阅:unsubscribe() ,后面在dispatch就监听不到了

  • 练习代码如下:

    js 复制代码
    const { createStore } = require("redux")
    // 初始化数据
    const initialState = {
      name: "shine",
      counter: 100
    }
    
    // 定义reducer函数: 纯函数
    // 两个参数: 
    // 参数一: store中目前保存的state
    // 参数二: 本次需要更新的action(dispatch传入的action)
    // 返回值: 它的返回值会作为store之后存储的state
    function renducer(state = initialState, action) {
      // console.log('renducer', state, action);
      switch (action.type) {
        case 'change_name':
          return {...state, name: action.name}
        case 'change_counter':
          return {...state, counter: state.counter + action.num}
        default:
          return state // 返回值会作为新的state值存到store中
      }
    }
    
    // 创建Store来存储这个state
    const store = createStore(renducer)
    
    // 订阅store中数据的变化,当数据变化时会执行回调并返回一个函数,调用就是取消订阅, store.getState()可以拿到store数据
    const unsubscribe = store.subscribe(()=> {
      console.log("订阅数据的变化:", store.getState())
    })
    
    const changeName1Action = ((name)=> ({type: 'change_name', name}))
    // 修改store中的数据,只能通过action修改
    store.dispatch(changeName1Action('changeName1')) // 派发action,会给到reducer的参数action
    
    store.dispatch({type: 'change_name', name: 'changeName2'})
    
    unsubscribe()
    // 取消订阅后,这里监听不到
    store.dispatch({type: 'change_counter', num: 5})

优化

上面使用将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护,我们需要对结构进行优化并对代码进行拆分,将store、reducer、action、constants拆分成一个个文件

  • 优化思路如下:

    • 将派发的action生成过程放到一个actionCreators函数中

    • 将定义的所有actionCreators的函数, 放到一个独立的文件中: actionCreators.js

    • actionCreatorsreducer函数中使用字符串常量是一致的,所以将常量抽取到一个独立constants的文件中

    • reducer和默认值(initialState)放到一个独立的reducer.js文件中, 而不是在index.js

  • 创建storeHoc/constants.js文件:

    js 复制代码
    const CHANGE_NAME = 'change_name'
    const CHANGE_COUNTER = 'change_counter'
    
    module.exports = {
      CHANGE_NAME,
      CHANGE_COUNTER
    }
  • 创建storeHoc/reducer.js文件:

    js 复制代码
    const { CHANGE_NAME, CHANGE_COUNTER } = require('./constants')
    
    const initialState = {
      name: 'shine',
      counter: 100
    }
    
    function reducer(state = initialState, action) {
      switch (action.type) {
        case CHANGE_NAME:
          return {...state, name: action.name}
        case CHANGE_COUNTER:
          return {...state, counter: state.counter + action.num}
        default:
          return state
      }
    }
    
    module.exports = reducer
  • 创建storeHoc/actionCreators.js文件:

    js 复制代码
    const { CHANGE_NAME, CHANGE_COUNTER } = require('./constants')
    
    const changeNameAction = (name => ({ type: CHANGE_NAME, name }))
    const changeCounterAction = (num => ({ type: CHANGE_COUNTER, num }))
    
    module.exports = {
      changeNameAction,
      changeCounterAction
    }
  • 创建storeHoc/index.js文件:

    js 复制代码
    const reducer = require('./reducer')
    
    const { createStore } = require('redux')
    
    const storeHoc = createStore(reducer)
    
    module.exports = storeHoc
  • index.js中使用优化后的代码修改store

    js 复制代码
    const storeHoc = require("./storeHoc");
    const { changeNameAction, changeCounterAction } = require('./storeHoc/actionCreators')
    
    const unsubscribeHoc = storeHoc.subscribe(()=>{
      console.log('优化后订阅数据的变化', storeHoc.getState());
    })
    storeHoc.dispatch(changeNameAction('优化name1'))
    storeHoc.dispatch(changeNameAction('优化name2'))
    storeHoc.dispatch(changeCounterAction(150))
    unsubscribeHoc()

融入react

目前reduxreact中使用是最多的,所以需要将上面编写的redux代码,融入到react当中去,在融入到过程中我们会学的很多的知识点,在这里新搭建一个项目

  • 脚手架搭建项目create-react-app learn_react_redux

  • 删除不必要文件,建立新文件 ,整体目录为下图:

    • 创建component文件夹

    • 创建store文件夹,安装redux后面使用 npm i redux

    • 创建craco.config.js文件配置less,步骤参考juejin.cn/post/747424...

      js 复制代码
      const CracoLessPlugin = require("craco-less");
      
      module.exports = {
        plugins: [
          {
            plugin: CracoLessPlugin,
            options: {
              lessLoaderOptions: {
                lessOptions: {
                  modifyVars: { "@primary-color": "#1DA57A" },
                  javascriptEnabled: true,
                },
              },
            },
          },
        ],
      };
    • 文件夹hocstoreRTK可以后面学习再创建

  • 接下来将redux融入react实现下面案例:

  • 创建store/constants.js文件:

    js 复制代码
    export const ADD_NUMBER = 'add_number'
    export const SUB_NUMBER = 'sub_number'
  • 创建store/reducer.js文件:

    js 复制代码
    import * as actionType from './constants'
    
    const initialState = {
      counter: 100,
    }
    function reducer(state=initialState, action) {
      switch (action.type) {
        case actionType.ADD_NUMBER:
          return {...state, counter: state.counter + action.num}
        case actionType.SUB_NUMBER:
          return {...state, counter: state.counter - action.num}
        default:
          return {...state}
      }
    }
    
    export default reducer
  • 创建store/actionCreators.js文件:

    js 复制代码
    import * as actionType from './constants'
    export const addNumber = (num)=>({type: actionType.ADD_NUMBER, num})
    export const subNumber = (num)=>({type: actionType.SUB_NUMBER, num})
  • 创建store/index.js文件:

    js 复制代码
    import { createStore } from "redux"
    import reducer from './reducer'
    
    const store = createStore(reducer)
    
    // 以前可以在这里监听和派发,但现在我们要不这些操作放到组件中
    // store.subscribe(() => { console.log(store.getState()) }) 
    // store.dispatch(addAction(10)) 
    // store.dispatch(subAction(5))
    export default store
  • 创建component/Lift.jsx文件做加法操作:

    js 复制代码
    import React, { PureComponent } from 'react'
    import store from '../store'
    import { addNumber } from '../store/actionCreators'
    
    export class Lift extends PureComponent {
      constructor() {
        super()
        this.state = {
          counter: store.getState().counter
        }
      }
      componentDidMount() {
        store.subscribe(()=> {
          this.setState({
            counter: store.getState().counter
          })
        })
      }
      addCounter(num) {
        store.dispatch(addNumber(num))
      }
      render() {
        return (
          <div>
            <h3>counter: {this.state.counter}</h3>
            <button onClick={e=>this.addCounter(1)}>+1</button>
            <button onClick={e=>this.addCounter(5)}>+5</button>
            <button onClick={e=>this.addCounter(10)}>+10</button>
          </div>
        )
      }
    }
    
    export default Lift
  • 创建component/Right.jsx文件做减法操作:

    js 复制代码
    import React, { PureComponent } from 'react'
    import store from '../store'
    import { subNumber } from '../store/actionCreators'
    
    export class Right extends PureComponent {
      constructor() {
        super()
        this.state = {
          counter: store.getState().counter
        }
      }
      componentDidMount() {
        store.subscribe(() => {
          const state = store.getState()
          this.setState({ counter: state.counter })
        })
      }
      componentWillUnmount() {
    
      }
      subCounter(num) {
        store.dispatch(subNumber(num))
      }
      render() {
        return (
          <div>        
            <h3>counter: {this.state.counter}</h3>
            <button onClick={e=>this.subCounter(11)}>-11</button>
            <button onClick={e=>this.subCounter(55)}>-55</button>
            <button onClick={e=>this.subCounter(100)}>-100</button>
          </div>
        )
      }
    }
    
    export default Right
  • app文件的处理 :可以先参考下面代码,都是后面要学习的知识

    js 复制代码
    // index.js
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      // <React.StrictMode> // 严格模式下接口会调用两次,这里注释掉
        <App />
      // </React.StrictMode>
    );
    
    
    
    // app.jsx
    import { Provider } from 'react-redux';
    import store from './store';
    import storeRTK from './storeRTK';
    import React, { PureComponent } from 'react'
    import { AppWrapper } from './style/app';
    import Lift from "./component/Lift";
    import Right from "./component/Right";
    import LiftConnect from './component/LiftConnect';
    import RightConnect from './component/RightConnect';
    import LiftHttp from './component/LiftHttp';
    import RightThunk from './component/RightThunk';
    import LiftModule from './component/LiftModule';
    import RightModule from './component/RightModule';
    import LiftToolkit from './component/LiftToolkit';
    import RightToolkit from './component/RightToolkit';
    import MyConnect from './component/MyConnect';
    import { StoreContext } from './hoc/StoreContext';
    
    export class App extends PureComponent {
      render() {
        return (
          <AppWrapper>
            <div className='top'>
              <h3>react融入redux</h3>
            </div>
            <div className='content'>
              <Lift />
              <Right />
            </div>
            
            {/* Provider联合connect使用 */}
            <Provider store={store}> 
              <div className='top'>
                <h3>react融入react-redux使用connect</h3>
              </div>
              <div className='content'>
                <LiftConnect />
                <RightConnect />
              </div>
              <div className='top'>
                <h3>react异步请求和中间件Thunk</h3>
              </div>
              <div className='content'>
                <LiftHttp />
                <RightThunk />
              </div>
              <div className='top'>
                <h3>react Module模块拆分</h3>
              </div>
              <div className='content'>
                <LiftModule />
                <RightModule />
              </div>
            </Provider>
            <Provider store={storeRTK}>
              <div className='top'>
                <h3>react 使用工具@reduxjs/toolkit</h3>
              </div>
              <div className='content'>
                <LiftToolkit />
                <RightToolkit />
              </div>
            </Provider>
            <Provider store={store}>
              <div className='top'>
                <h3>react 自己实现connect</h3>
              </div>
              <div className='content'>
                <StoreContext.Provider value={store}> 
                  <MyConnect />
                </StoreContext.Provider>
              </div>
            </Provider>
          </AppWrapper>
        )
      }
    }
    export default App
    
    
    
    // style/app.js
    const { styled } = require("styled-components");
    
    export const AppWrapper = styled.div`
      display: flex;
      flex-direction: column;
      .top {
        width: 100%;
        padding: 0 14px;
        margin: 14px 0;
        border: 1px solid orange;
      }
      .content {
        display: flex;
        width: 100%;
        > * {
          flex: 1;
          padding: 14px;
          border: 1px solid pink;
        }
      }
    ` 
  • redux在开发中工作的流程如下图:

react-redux

在上面案例将redux简单融入了react,虽然功能上可以实现状态管理和更新,但还有很多缺点需要我们解决:

  • 直接依赖 store,耦合性高

    • 组件直接导入依赖 Reduxstore,导致组件与 Redux 强耦合

    • 如果未来需要替换状态管理工具(如使用 MobX 或 Context API),需要修改所有直接依赖 store 的组件,组件难以复用

  • 手动订阅和更新状态,代码冗余

    • 使用 store.subscribe 手动订阅状态变化,并在回调中调用 setState 更新组件状态

    • 代码冗余,每个组件都需要写类似的订阅逻辑,容易遗漏取消订阅的逻辑,可能导致内存泄漏

  • 性能问题

    • 每次 store 状态变化时,都会触发 setState,即使组件依赖的状态没有变化,可能导致不必要的重新渲染
  • 状态更新逻辑分散

    • 状态更新逻辑分散在组件的 constructorcomponentDidMountaddCounter 方法中,可读性和可维护性差,难以追踪状态更新逻辑
  • 缺乏类型安全

    • 直接操作 store.getState()store.dispatch,缺乏类型检查和提示

使用

react-redux解决了上面的缺点,主要知识点如下,还有其他知识点后面会一一学习:

  • Provider 组件 :用于将 Reduxstore 注入到 React 应用中,使所有子组件都能访问 store

  • connect 函数 :用于将 React 组件与 Reduxstore 连接起来,使组件能够访问状态和派发 actions

    • connect(mapStateToProps, mapDispatchToProps)(Component)

    • mapStateToProps :将 state 映射到 props

    • mapDispatchToProps :将 dispatch 映射到 props

    • Component :需要连接的 React 组件

  • useSelector :用于从 store 中获取状态,函数组件推荐

    js 复制代码
    import { useSelector } from 'react-redux';
    function Counter() {
      const counter = useSelector((state) => state.counter);
      return <div>Counter: {counter}</div>;
    }
    • const stateValue = useSelector(selectorFunction, equalityFunction)

    • selectorFunction:一个函数,接收整个 Redux state 并返回需要的部分状态

    • equalityFunction(可选):用于比较前后状态的函数,默认使用严格相等(===

    • 每次 state 变化时,useSelector 都会重新执行 ,如果返回的值没有变化,组件不会重新渲染,如果需要提取多个状态,可以调用多次 useSelector

    • 如果 useSelector 返回的对象或数组是新的引用(即使内容没有变化),组件也会重新渲染。可以使用 shallowEqual 进行比较优化

      js 复制代码
      import { useSelector, shallowEqual } from 'react-redux';
      
      const { counter, user } = useSelector((state) => ({
        counter: state.counter,
        user: state.user,
      }), shallowEqual);
  • useDispatchdispatch 函数是稳定的,不会随组件渲染而变化,因此可以安全地用于 useEffect 或其他 Hooks

    js 复制代码
    import { useDispatch } from 'react-redux';
    function Counter() {
      const dispatch = useDispatch();
      const increment = () => dispatch({ type: 'INCREMENT' });
      return (
        <div>
          <button onClick={increment}>Increment</button>
        </div>
      );
    }
  • 优化LiftRight的新组件如下app.jsx中需要先用<Provider store={store}></Provider>包裹下面两个组件,使所有子组件都能访问 store

    js 复制代码
    // 创建component/LiftConnect.jsx文件如下
    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { addNumber } from '../store/actionCreators'
    
    export class LiftConnect extends PureComponent {
      render() {
        return (
          <div>
            <h3>counter: {this.props.number}</h3>
            <button onClick={e=>this.addNumberConnect(18)}>+18</button>
            <button onClick={e=>this.addNumberConnect(58)}>+58</button>
            <button onClick={e=>this.addNumberConnect(108)}>+108</button>
          </div>
        )
      }
      addNumberConnect(num) {
        this.props.addNumberConnect(num)
      }
    }
    
    /* 
      1. 使得 store 对于我们的应用是可见的。使用 React Redux 提供的 API <Provider /> 去包裹我们的应用 index.js 
      2. mapStateToProps将拿到的store数据存到props中,展示可以通过props去拿
      s. mapDispatchToProps派发函数存到props中,展示可以通过props去执行action函数
    */
    
    const mapStateToProps = state=> ({
      number: state.counter
    })
    
    const mapDispatchToProps = dispatch=> ({
      addNumberConnect(num){
        dispatch(addNumber(num))
      }
    })
    
    export default connect(mapStateToProps, mapDispatchToProps)(LiftConnect)
    
    
    
    // 创建component/RightConnect.jsx文件如下
    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { subNumber } from '../store/actionCreators'
    
    export class RightConnect extends PureComponent {
      render() {
        return (
          <div>
            <div>
            <h3>counter: {this.props.number}</h3>
            <button onClick={e=>this.subNumberConnect(18)}>-18</button>
            <button onClick={e=>this.subNumberConnect(58)}>-58</button>
            <button onClick={e=>this.subNumberConnect(108)}>-108</button>
          </div>
          </div>
        )
      }
      subNumberConnect(num) {
        this.props.subNumberConnect(num)
      }
    }
    
    const mapStateToProps = state=>({
      number: state.noModule.counter
    })
    
    const mapDispatchToProps = dispatch=>({
      subNumberConnect(num){
        dispatch(subNumber(num))
      }
    })
    
    export default connect(mapStateToProps, mapDispatchToProps)(RightConnect) 

异步操作

  • 在前面案例中,redux中保存的counter是一个本地定义的数据,可以直接通过同步的操作来dispatch actionstate就会被立即更新

  • 但是真实开发中,redux中保存的很多数据可能来自服务器,需要进行异步的请求,再将数据保存到redux

  • 网络请求可以在class组件的componentDidMount中发送,流程如下:

  • 下载axiosnpm i axios

  • store/constants.js中添加常量:

    js 复制代码
    export const ADD_NUMBER = 'add_number'
    export const SUB_NUMBER = 'sub_number'
    export const CHANGE_BANNERS = 'change_banners'
    export const CHANGE_RECOMMENDS = 'change_recommends'
  • store/actionCreateors.js中添加changeBanners

    js 复制代码
    import axios from 'axios'
    import * as actionType from './constants'
    
    export const addNumber = (num)=>({type: actionType.ADD_NUMBER, num})
    export const subNumber = (num)=>({type: actionType.SUB_NUMBER, num})
    export const changeBanners = (banners)=>({type: actionType.CHANGE_BANNERS, banners})
    export const changeRecommends = (recommends)=>({type: actionType.CHANGE_RECOMMENDS, recommends})
  • store/reducer.js中添加banners

    js 复制代码
    import * as actionType from './constants'
    
    const initialState = {
      counter: 100,
      banners: [],
      recommends: [],
    }
    function reducer(state=initialState, action) {
      switch (action.type) {
        case actionType.ADD_NUMBER:
          return {...state, counter: state.counter + action.num}
        case actionType.SUB_NUMBER:
          return {...state, counter: state.counter - action.num}
        case actionType.CHANGE_BANNERS:
          return {...state, banners: action.banners}
        case actionType.CHANGE_RECOMMENDS:
          return {...state, recommends: action.recommends}
        default:
          return {...state}
      }
    }
    export default reducer
  • component/LiftHttp.jsx组件如下:

    js 复制代码
    import axios from 'axios'
    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { changeBanners } from '../store/actionCreators'
    
    export class LiftHttp extends PureComponent {
      componentDidMount(){
        axios.get('http://123.207.32.32:8000/home/multidata').then(res=> {
          this.props.changeHttpBanners(res.data.data.banner.list)
        })
      }
      render() {
        return (
          <div>
            <ul>
            {
              this.props.banners.map(item=> {
                return <li key={item.acm}>{item.title}</li>
              })
            }
            </ul>
          </div>
        )
      }
    }
    
    const mapStateToProps = state=> ({
      banners: state.banners
    })
    const mapDispatchToProps = dispatch=> ({
      changeHttpBanners(banners) {
        dispatch(changeBanners(banners))
      }
    })
    
    export default connect(mapStateToProps,mapDispatchToProps)(LiftHttp) 

redux-thunk

上面的案例代码有一个缺陷,需要将网络请求的异步代码放到组件的生命周期中来完成,事实上网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理,流程如下图:

但是在redux中如何可以进行异步的操作呢?

  • 就是使用中间件(Middleware),学习过ExpressKoa框架的应该对中间件的概念不陌生

  • 中间件可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作

  • redux也引入了中间件(Middleware)的概念,目的是在dispatchaction和最终达到的reducer之间,扩展一些自己的代码,比如日志记录、调用异步接口、添加代码调试功能等等

  • 官网推荐的包括演示网络请求的中间件是 redux-thunk

如何使用redux-thunk

  • 安装redux-thunknpm i redux-thunk

  • store/index.js引入thunkimport thunk from "redux-thunk"

  • 在创建store时传入应用了中间件的enhance函数:const store = createStore(reducer, applyMiddleware(thunk))

  • componentDidMount中的请求放store/actionCreators.js中:

    js 复制代码
    export const fetchThunkRecommendsAction = (()=> {
      return function(dispatch, getState) {
        axios.get('http://123.207.32.32:8000/home/multidata').then(res=> {
          dispatch(changeRecommends(res.data.data.recommend.list))
        })
      }
    })
  • 创建component/RightThunk.jsx

    js 复制代码
    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { fetchThunkRecommendsAction } from '../store/actionCreators'
    
    export class RightThunk extends PureComponent {
      componentDidMount() {
        this.props.fetchThunkRecommends()
      }
      render() {
        return (
          <div>
            <ul>
              {this.props.recommends.map(item=> {
                return <li key={item.sort}>{item.title}</li>
              })}
            </ul>
          </div>
        )
      }
    }
    
    const mapStateToProps = state=>({
      recommends: state.recommends
    })
    
    const mapDispatchToProps = dispatch=> ({
      fetchThunkRecommends() {
        dispatch(fetchThunkRecommendsAction())
      }
    })
    
    export default connect(mapStateToProps,mapDispatchToProps)(RightThunk)

redux-devtools

redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?

  • redux官网提供了redux-devtools的工具,利用这个工具,可以知道每次状态是如何被修改的,修改前后的状态变化等等

  • 安装该工具需要两步:

    • 在对应的浏览器中安装相关的插件 (比如Chrome浏览器扩展商店中搜索Redux DevTools即可)

    • redux中继承devtools的中间件 ,在store/index.js添加代码:

      js 复制代码
      const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
      const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件

拆分模块

在上面的案例中,一个reducer中包含了好几个组件的state数据,以后会越来越臃肿,在实际开发中,项目会有很多个页面,每个页面都需要有自己的reducer,这时候就要拆分模块,这里就不拆分以前的代码了,我们重新写两个组件并拆分reducer来模拟两个页面的拆分

  • 创建store/liftstore/right文件夹,里面放页面用到的actionCreators、constants、reducer、index(主要导出reduceractionCreators),这里就不说了,文件都和上面例子一样

  • 修改store/index.js文件:

    js 复制代码
    import { createStore, combineReducers } from "redux";
    import noModuleReducer from "./reducer";
    import liftReducer from "./lift";
    import rightReducer from "./right";
    import thunk from "redux-thunk"
    
    // const store = createStore(reducer)
    const reducer = combineReducers({
      liftModule: liftReducer,
      rightModule: rightReducer,
      noModule: noModuleReducer, // 以前用到的这个reducer的地方要修改成state.noModule.counter来获取数据
    });
    
    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
    const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件
    
    export default store;
    • redux给提供了一个combineReducers函数可以方便的让对多个reducer进行合并

    • 那么combineReducers是如何实现的呢?

      事实上,它也是将传入的reducers合并到一个对象中,最终返回一个combination的函数

      在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state

      新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新

      js 复制代码
      // combineReducers实现原理(了解)
      function combineReducers(state = {}, action) {
        // 返回一个对象, store的state
        return {
          counter: counterReducer(state.counter, action),
          home: homeReducer(state.home, action),
          user: userReducer(state.user, action)
        }
      }
  • 新建component/RightModule.jsx文件:

    js 复制代码
    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { fetchRightRecommends } from '../store/right'
    
    export class RightModule extends PureComponent {
      componentDidMount() {
        this.props.fetchRecommends()
      }
      render() {
        return (
          <div>
            <ul>
            {
              this.props.recommends.map(item=> {
                return <li key={item.sort}>{item.title}</li>
              })
            }
            </ul>
          </div>
        )
      }
    }
    
    // 只会找store下面的index文件(取决于你Provider是传入的store引入的那个store/index文件),若此文件中没有合并则无效
    const mapStateToProps = state=> ({
      recommends: state.rightModule.recommends
    })
    
    const mapDispatchToProps = dispatch=>({
      fetchRecommends() {
        dispatch(fetchRightRecommends())
      }
    })
    
    export default connect(mapStateToProps,mapDispatchToProps)(RightModule) 

Redux Toolkit

  • 学习Redux的时候应该发现,redux的编写逻辑过于的繁琐和麻烦

  • 代码通常拆在多个文件中,Redux Toolkit包旨在成为编写Redux逻辑的标准方式,减少模板代码

  • 从而解决上面提到的问题,在很多地方为了称呼方便,也将之称为RTK

  • Redux Toolkit(RTK)内置了 Immer,底层使用了ImmutableJS的一个库来保证数据的不可变性,让你可以直接修改 stateRTK 内部会自动处理不可变性

  • Immer通过 Proxy 监听 state 的变化,在 produce() 结束后,生成新的不可变对象

  • 先安装Redux Toolkitnpm install @reduxjs/toolkit

核心API

Redux ToolkitRedux 开发更加高效,核心API主要有下面几个,实现图中案例来理解一下核心API

  • configureStore :简化 store 配置,自动集成 Redux DevTools 和中间件,主要包含如下几个参数:

    js 复制代码
    // 创建storeRTK/index.js
    import { configureStore } from "@reduxjs/toolkit";
    import liftRenducer from "./features/lift";
    import rightRenducer from "./features/right";
    
    const store = configureStore({
      // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), // 添加中间件 
      // middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }),
      // devTools: process.env.NODE_ENV !== "production", // 仅在开发环境启用 Redux DevTools
      reducer: {
        lift: liftRenducer,
        right: rightRenducer,
      },
    });
    
    export default store;
    • reducer :将slice中的reducer可以组成一个对象传入此处

    • middleware:可以使用参数,传入其他的中间件

      getDefaultMiddleware() 默认包含 redux-thunkserializableStateInvariantimmutableStateInvariant

      你可以用 .concat() 添加自定义中间件,比如 redux-logger,也可以使用 .prepend() 在默认中间件前添加

      serializableCheck: false 适用于 Redux 状态中包含不可序列化值(如 MapSetDate

    • devTools :是否配置devTools工具,默认为true,可以手动设置 false 来关闭它,或根据环境变量控制(如 process.env.NODE_ENV

  • createSlice :一步创建 reduceraction

    js 复制代码
    // 创建storeRTK/features/lift.js
    import { createSlice } from "@reduxjs/toolkit";
    
    const lifteSlice = createSlice({
      name: 'lift',
      initialState: {
        counter: 888
      },
      reducers: {
        // addCounter(state, action) {
        //   state.counter = state.counter + action.payload
        // },
        addCounter(state, { payload }) {
          state.counter = state.counter + payload
        }
      }
    })
    
    export const { addCounter } = lifteSlice.actions
    export default lifteSlice.reducer
    
    
    
    // 创建component/LiftToolkit.jsx
    import React, { PureComponent } from 'react'
    import { connect } from 'react-redux'
    import { addCounter } from '../storeRTK/features/lift'
    
    export class LiftToolkit extends PureComponent {
      render() {
        return (
          <div>
            <h3>counter: {this.props.counter}</h3>
            <button onClick={e=>this.addCounterNum(1)}>+1</button>
            <button onClick={e=>this.addCounterNum(5)}>+5</button>
            <button onClick={e=>this.addCounterNum(10)}>+10</button>
          </div>
        )
      }
      addCounterNum(num) {
        this.props.addCounterNum(num)
      }
    }
    
    const mapStateToProps = state=> ({
      counter: state.lift.counter
    })
    const mapDispatchToProps = dispatch => ({
      addCounterNum(num) {
        dispatch(addCounter(num))
      }
    })
    
    export default connect(mapStateToProps, mapDispatchToProps)(LiftToolkit)
  • createAsyncThunk :处理异步请求(如 API 调用),简化 redux-thunk

    • createAsyncThunk创建出来的actiondispatch时,会存在三种状态:

      pendingaction被发出,但是还没有最终的结果

      fulfilled:获取到最终的结果(有返回值的结果)

      rejected:执行过程中有错误或者抛出了异常

      js 复制代码
      import { createAsyncThunk } from "@reduxjs/toolkit";
      
      export const 异步Action名 = createAsyncThunk(
        "命名空间/Action名",  // 唯一标识,devTools工具会显示
        async (参数, thunkAPI) => {
          try {
            const response = await fetch("你的API地址");
            return await response.json(); // 返回数据
          } catch (error) {
            return thunkAPI.rejectWithValue(error.message); // 失败处理
          }
        }
      );
    • 可以在createSliceentraReducer中监听三种状态extraReducer还可以传入一个函数,函数接受一个builder参数,可以向builder中添加case来监听异步操作的结果

      js 复制代码
      // 创建storeRTK/features/right.js
      import axios from "axios";
      
      const { createSlice, createAsyncThunk } = require("@reduxjs/toolkit");
      
      export const fetchRightBanners = createAsyncThunk(
        "right/fetchBanners",
        async (extraInfo, store) => {
          let { data } = await axios.get("http://123.207.32.32:8000/home/multidata");
      
          /* 
              修改state.banners的值
                1. return 值 ----> [fetchRightBanners.fulfilled](state, action) {赋值}
                2. return 值 ----> 通过bulid.addCase(fetchRightBanners.fulfilled,(state, action)=>{赋值}
                3. 不return直接使用dispatch修改createAsyncThunk回调函数接受两个参数extraInfo:调用时传的值,store
          */
      
          // console.log(data, extraInfo, store);
          store.dispatch(changeBanners(data.data.banner.list));
          // return data.data.banner.list // 不return将拿不到值,这里return的值会作为[fetchRightBanners.fulfilled]的action.payloa的值
        }
      );
      const rightSlice = createSlice({
        name: "right",
        initialState: {
          banners: [],
        },
        reducers: {
          changeBanners(state, { payload }) {
            state.banners = payload;
          },
        },
      
        /* 
            extraReducers: {
              [fetchRightBanners.pending](state, action) {
                console.log(state, action);
              },
              [fetchRightBanners.fulfilled](state, action) {
                console.log(state, action);
                state.banners = action.payload 
              },
              [fetchRightBanners.rejected](state, action) {
                console.log(state, action);
              }
            } 
        */
      
        /* 
            extraReducers(bulid) {
              bulid.addCase(fetchRightBanners.pending,(state, action)=>{
                console.log('padding', state, action);
              }).addCase(fetchRightBanners.fulfilled,(state,action)=> {
                console.log('fulfilled', state, action);
              }).addCase(fetchRightBanners.rejected,(state, action)=> {
                console.log('rejected', state, action);
              })
            } 
        */
      
      });
      
      export const { changeBanners } = rightSlice.actions;
      export default rightSlice.reducer;
      
      
      
      // 创建component/RightToolkit.jsx
      import React, { PureComponent } from 'react'
      import { connect } from 'react-redux'
      import { fetchRightBanners } from '../storeRTK/features/right'
      
      export class RightToolkit extends PureComponent {
        componentDidMount() {
          this.props.fetchBanners()
        }
        render() {
          return (
            <div>
              <ul>
                {
                  this.props.banners.map(item=> {
                    return <li key={item.acm}>{item.title}</li>
                  })
                }
              </ul>
            </div>
          )
        }
      }
      
      const mapStateToProps = state=> ({
        banners: state.right.banners
      })
      
      const mapDispatchToProps = dispatch=> ({
        fetchBanners() {
          dispatch(fetchRightBanners())
        }
      })
      
      export default connect(mapStateToProps, mapDispatchToProps)(RightToolkit) 

原理学习

redux的很多知识基本已经学完,下面是一些有利于我们更好的理解redux的练习

connect实现

  • connectReact-Redux提供的高阶组件(HOC),用于将 Redux 状态和 action 绑定到 React 组件,使组件能够访问 Redux store 并派发 action

  • 虽然 Redux Toolkit (RTK) 推荐使用 useSelectoruseDispatch 代替 connect,但在某些类组件或旧项目中,connect 仍然广泛使用

  • 理解connect特性:

    • connect(...) 返回一个高阶组件(HOCconnect(mapStateToProps, mapDispatchToProps)(组件)

    • connect 通过 context 获取 store

      connect不会直接接收 store,而是依赖 React-Redux 提供的 context

      只有组件在 Provider 作用域下,connect 才能找到 store

    • connect 订阅 store 更新

      connect 内部使用 store.subscribe() 监听 Redux state 变化,并重新渲染组件

  • 理解透彻后那么我们来自己实现connect

    js 复制代码
    // 创建hoc/StoreContext.js
    import { PureComponent } from "react"
    // 解耦store
    // import store from '../store'
    import { StoreContext } from './StoreContext'
    
    export function connect(mapStateToProps,mapDispathToProps) {
      return function(OriginWrapper){
        class NewWrapper 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() {
            // mapStateToProps,mapDispathToProps传进来的都是函数,需要调用并传入参数,才能拿到函数返回值
            const stateObj = mapStateToProps(this.context.getState())
            const dispatchObj = mapDispathToProps(this.context.dispatch)
            return <OriginWrapper {...this.props} {...stateObj} {...dispatchObj} />
          }
        }
        NewWrapper.contextType = StoreContext
        return NewWrapper
      }
    }
    
    
    
    // 创建hoc/connect.js
    const { createContext } = require("react");
    export const StoreContext = createContext()
    
    
    
    // 创建component/MyConnect.jsx使用自己写的connect
    import React, { PureComponent } from 'react'
    import { fetchLiftBanners } from '../store/lift'
    import { connect } from '../hoc/connect'
    
    export class MyConnect extends PureComponent {
      componentDidMount() {
        this.props.fetchBanners()
      }
      render() {
        return (
          <div>
            <ul>
            { 
              this.props.banners.map(item=> {
                return <li key={item.acm}>{item.title}</li>
              })
            }
            </ul>
          </div>
        )
      }
    }
    
    const mapStateToProps = state=> ({
      banners: state.liftModule.banners
    })
    
    const mapDispatchToProps = dispatch=>({
      fetchBanners() {
        dispatch(fetchLiftBanners())
      }
    })
    export default connect(mapStateToProps, mapDispatchToProps)(MyConnect) 
    
    
    // app.js引用组件
    <StoreContext.Provider value={store}> 
      <MyConnect />
    </StoreContext.Provider>

中间件实现

中间件的目的是在redux中插入一些自己的操作

  • 现在有一个需求在dispatch之前打印一下本次的action对象,dispatch完成之后可以打印一下最新的store state,如果没有中间件怎么实现呢?

    js 复制代码
    // 创建store/thunk/log.js
    /* 
      逻辑反推梳理:
      store.dispatch(addNumber(num))
      可见store.dispatch是一个函数,参数也是一个函数
      addNumber = (num)=>({type: actionType.ADD_NUMBER, num})
      而addNumber也是一个函数返回对象则
      store.dispatch(addNumber(num)) = store.dispatch({type: actionType.ADD_NUMBER, num})
      由此可知 store.dispatch = logDispacth
      logDispacth也必须是一个函数,并且接受了对象参数相当于{type: actionType.ADD_NUMBER, num}
      修改时就可以执行store.dispatch穿进去这个参数,并派发前后打印log
    */
    
    function log(store) {
      let init = store.dispatch
      function logDispacth(action){
        console.log('派发之前', store.getState());
        init(action)
        console.log('派发之后', store.getState());
      }
      // 我们可以利用一个hack一点的技术:Monkey Patching(猴补丁),利用它可以修改原有的程序逻辑
      store.dispatch = logDispacth
    }
    export default log
  • 那么就可以自己实现thunk中间件

    js 复制代码
    // 创建store/thunk/thunk.js
    function thunk(store) {
      let init = store.dispatch
      function myThunk(action) {
        if(typeof action == 'function') {
          action(init, store.getState)
        }else {
          init(action)
        }
      }
      store.dispatch = myThunk
    }
    export default thunk
  • 实现合并中间件的函数

    js 复制代码
    // 创建store/thunk/middleware.js
    function middleware(store, ...fn) {
      fn.forEach(f=>{
        f(store)
      })
    }
    export default middleware
  • 将三个函数全部导出

    js 复制代码
    // 创建store/thunk/index.js
    import log from './log'
    import thunk from './thunk'
    import middleware from './middleware'
    
    export {
      log,
      thunk,
      middleware
    }
  • 使用自己封装的中间件

    js 复制代码
    // store/index.js
    import { createStore, combineReducers } from "redux"
    import liftReducer from './lift'
    import rightReducer from './right'
    // import thunk from "redux-thunk"
    import { middleware, log, thunk } from "./thunk"
    
    
    // const store = createStore(reducer)
    const reducer = combineReducers({
      liftModule: liftReducer,
      rightModule: rightReducer,
    })
    
    // const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
    // const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件
    const store = createStore(reducer)
    // 自己实现派发前后打印日志和中间件
    middleware(store, log, thunk)
    export default store
  • 当然真实的中间件实现起来肯定会更加的灵活,有兴趣可以参考redux合并中间件的源码流程

state管理方案

目前已经主要学习了三种状态管理方式

  • 组件中自己的state管理

  • Context数据的共享状态

  • Redux管理应用状态

  • 在开发中如何选择呢?Redux的作者有给出自己的建议

  • 目前自己采用的state管理方案:

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

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

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

相关推荐
豐儀麟阁贵6 分钟前
8.5在方法中抛出异常
java·开发语言·前端·算法
zengyuhan50336 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休39 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running1 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔1 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654261 小时前
Android的自定义View
前端
WILLF1 小时前
HTML iframe 标签
前端·javascript
枫,为落叶1 小时前
Axios使用教程(一)
前端
小章鱼学前端1 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah1 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript