彻底理解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来维护

相关推荐
Fantasywt3 小时前
THREEJS 片元着色器实现更自然的呼吸灯效果
前端·javascript·着色器
IT、木易3 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
Mr.NickJJ4 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
张拭心5 小时前
2024 总结,我的停滞与觉醒
android·前端
念九_ysl5 小时前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖6 小时前
vue3如何配置环境和打包
前端·javascript·vue.js
Mr.NickJJ6 小时前
React Native v0.78 更新
javascript·react native·react.js
星之卡比*6 小时前
前端知识点---库和包的概念
前端·harmonyos·鸿蒙
灵感__idea6 小时前
Vuejs技术内幕:数据响应式之3.x版
前端·vue.js·源码阅读
烛阴6 小时前
JavaScript 构造器进阶:掌握 “new” 的底层原理,写出更优雅的代码!
前端·javascript