Skip to content

S13-04 React-Redux

[TOC]

API

Store

store是 Redux 中的一个对象,表示整个应用程序的状态存储

  • createStore(reducer, preloadedState?, enhancer? )返回:store,用于创建 Redux store 的函数

    • 参数
    • reducer(state, action) => void,用于处理 Redux 应用程序状态的函数,并在 action 被 dispatch 时更新状态
    • preloadedStateObject,用于初始化 Redux 应用程序状态的对象
    • enhancerFunction,增强 Redux store 功能的函数,比如使用中间件、使用调试工具等
    • 返回
    • storeObject,store 是一个存储应用程序状态的对象,可以帮助管理应用程序的状态,并在状态变化时更新 UI。
  • store.getState(void)返回:,获取当前的状态树。

  • store.dispatch(action)返回:,用于触发状态变化。它接受一个表示状态变化的 action 对象作为参数,并将该对象传递给 Redux 的 reducer 函数,从而更新应用程序的状态。

    • 参数
    • action{ type },一个普通的 JavaScript 对象,用于表示状态变化的动作。它必须包含一个type属性,用于指定状态变化的类型
  • store.subscribe(listener)返回:unsubscribe,用于订阅状态变化。它接受一个回调函数作为参数,该回调函数会在每次状态发生变化时被调用。

    • 参数
    • listener() => void, 回调函数,用于处理状态变化的逻辑。
    • 返回
    • unsubscribe() => void,取消订阅的函数,调用该方法取消订阅: unsubscribe()
  • combineReducers(reducers)返回:reducer,用于合并多个 reducer 的函数

    • 参数
    • reducers{reducer,...},一个由多个 reducer 组成的对象,每个 reducer 都是一个函数,用于处理不同的 state
    • 返回
    • reducer(state, action) => void,合并后的 reducer 函数

React-Redux

React-Redux是一个将 React 和 Redux 结合起来使用的库,它提供了一种将Redux 的状态管理React 组件结合起来的方式。

  • <Provider>返回:,作用是将 Redux store 注入到 React 应用中,使得所有的组件都可以访问到 store 中的状态。
    • 属性
    • store<Provider store={store}> <App/> </Provider>,将 store 作为 Provider 组件的 props 传入
  • connect(mapStateToProps, mapDispatchToProps?)(Cpn)返回:,作用是将 React 组件和 Redux store 连接起来。通过 connect()函数,React 组件可以访问到 store 中的状态,并且在状态变化时自动更新组件。
    • 参数
    • mapStateToProps(state) => void,将 Redux store 中的状态映射到组件的 props 中
    • mapDispatchToProps(dispatch) => void,将 dispatch 方法映射到组件的 props 中
    • Cpn组件,被连接的 React 组件

Redux Toolkit

Redux Toolkit 的核心 API主要是如下几个:

  • configureStore({ reducer, middleware, devTools, ... })返回:store,包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。
    • 参数
    • reducer:``,Redux store 的根 reducer
    • middleware:``,要使用的中间件数组
    • devTools:``,是否启用开发工具(如 Redux DevTools),默认 true
    • preloadedState:``,初始状态
    • enhancers:``,其他 store 增强器
    • 返回
    • store:``,返回的是一个 Redux store 实例,而不是一个类。因此无法创建多个 store 实例
  • createSlice({ name, initialState, reducers,... })返回:reducerSlice,用于创建一个 Redux reducer 和 action creator 的集合
    • 参数
    • nameString,用于标识这个 reducer 的名称,action.type会根据 name 生成
    • initialStateany,表示这个 reducer 的初始状态
    • reducers{ reducer,... },用于定义这个 reducer 的 action creator 和对应的 reducer 函数
      • reducer(state, action) => void,相当于之前的 reducer 函数
    • extraReducers: { reducer,... },包含了与当前 slice 无关的 reducer 函数
      • reducer(state, action) => void,相当于之前的 reducer 函数
    • 返回
    • reducerSlice:``,返回一个 reducer 片段
  • createAsyncThunk(typePrefix, payloadCreator, options? )返回:,用于创建一个异步 action creator
    • 参数
    • typePrefixString,用于标识这个异步 action creator 的类型前缀
    • payloadCreator(arg, thunkAPI) => Promise,用于处理异步操作并返回一个 Promise 对象
    • options?Object,用于配置异步 action creator 的一些选项
      • fulfilled:用于指定异步操作成功时的处理函数。
      • rejected:用于指定异步操作失败时的处理函数。
      • pending:用于指定异步操作进行中时的处理函数。
      • dispatchCondition:用于指定在什么条件下才会 dispatch 这个 action 的函数。
      • condition:用于指定在什么条件下才会调用payloadCreator函数的函数。
      • typeSuffixes:用于指定异步 action creator 的类型后缀的对象。
      • serializeError:用于指定如何序列化异步操作的错误信息的函数。

image-20230414182149658

Redux 核心思想

理解 JavaScript 纯函数

函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数式编程的范式,所以也有纯函数的概念

  • 在 react 开发中纯函数是被多次提及的;

  • 比如 react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个reducer的概念,也是要求必须是一个纯函数

  • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;

纯函数的维基百科定义:

  • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:

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

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

  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

当然上面的定义会过于的晦涩,所以我简单总结一下:

  • 确定的输入,一定会产生确定的输出

  • 函数在执行过程中,不能产生副作用;(如触发事件)

副作用概念的理解

那么这里又有一个概念,叫做副作用,什么又是副作用呢?

  • *副作用(side effect)*其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;

  • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储

纯函数在执行的过程中就是不能产生这样的副作用:

  • 副作用往往是产生 bug 的 “温床”

纯函数的案例

我们来看一个对数组操作的两个函数:

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

  • splice:splice 截取数组, 会返回一个新的数组, 也会对原数组进行修改;

slice 就是一个纯函数,不会修改数组本身,而 splice 函数不是一个纯函数;

js
const arr = ['Tom', 'Jack', 'Jerry', 'John']

// 1. slice 不会修改原数组,并返回被删除的元素,是纯函数
const res1 = arr.slice(1, 2)
console.log('arr: ', arr, 'res1: ', res1) // arr:  (4) ['Tom', 'Jack', 'Jerry', 'John'] res1:  ['Jack']

// 2. splice 会修改原数组,并返回被删除的元素
const res2 = arr.splice(1, 1)
console.log('arr: ', arr, 'res2: ', res2) // arr:  (3) ['Tom', 'Jerry', 'John'] res2:  ['Jack']

判断下面函数是否是纯函数?

是纯函数

js
// 纯函数
function foo(num1, num2) {
  return num1 + num2
}

不是纯函数

js
    // 2. 不是纯函数
+    let x = 10
    function bar(num) {
+      return num + x
    }
    console.log(bar(20))
    x = 20
    console.log(bar(20))

不是纯函数

js
    // 3. 不是纯函数
    const info = { name: 'Jack', age: 10 }
    function baz(info) {
+      info.name = 'Tom'
    }
    baz(info)
    console.log(info)

纯函数的作用和优势

为什么纯函数在函数式编程中非常重要呢?

  • 因为你可以安心的编写和安心的使用

  • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;

  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

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

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

为什么需要 redux

JavaScript 开发的应用程序,已经变得越来越复杂了:

  • JavaScript 需要管理的状态越来越多,越来越复杂;

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

管理不断变化的 state 是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View 页面也有可能会引起状态的变化;

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

React 是在视图层帮助我们解决了 DOM 的渲染过程,但是 State 依然是留给我们自己来管理:

  • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;

  • React主要负责帮助我们管理视图state如何维护最终还是我们自己来决定

image-20230404111336581

Redux就是一个帮助我们管理 State 的容器:Redux 是 JavaScript 的状态容器,提供了可预测的状态管理;

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

Redux 的核心理念 - Store

Redux 的核心理念非常简单。

比如我们有一个朋友列表需要管理:

  • 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;

  • 比如页面的某处通过 products.push 的方式增加了一条数据;

  • 比如另一个页面通过 products[0].age = 25 修改了一条数据;

整个应用程序错综复杂,当出现 bug 时,很难跟踪到底哪里发生的变化;

image-20230404111437002

Redux 的核心理念 - action

Redux 要求我们通过 action 来更新数据

  • 所有数据的变化,必须通过派发(dispatch)action 来更新

  • action 是一个普通的 JavaScript 对象,用来描述这次更新的typecontent

比如下面就是几个更新 friends 的 action:

  • 强制使用 action 的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;

  • 当然,目前我们的 action 是固定的对象;

  • 真实应用中,我们会通过函数来定义,返回一个 action;

image-20230404111446672

Redux 的核心理念 - reducer

但是如何将 state 和 action 联系在一起呢?答案就是reducer

  • reducer 是一个纯函数

  • reducer 做的事情就是将传入的 state 和 action 结合起来生成一个新的 state

image-20230404111514037

Redux 基本使用

Redux 测试项目搭建

安装 redux:

sh
npm install redux --save
# 或
yarn add redux

1.创建一个新的项目文件夹:learn-redux

sh
# 执行初始化操作
yarn init
# 安装redux
yarn add redux

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

3.修改 package.json 可以执行 index.js

js
"scripts": {
  "start": "node src/index.js"
}

Redux 的基本使用

1、创建一个对象,作为我们要保存的状态:

js
// 1. state对象保存状态
const initialState = {
  name: 'Tom',
  age: 10
}

2、创建 Store 来存储这个 state

  • 创建 store 时必须创建 reducer;

    js
      import { createStore } from 'redux'
      // 1. state对象保存状态
      const initialState = {
        name: 'Tom',
        age: 10
      }
    
      // 2. reducer纯函数
    +  function reducer(state = initialState, action) {
        return state
      }
    
      // 3. 创建store
    +  const store = createStore(reducer)
    
      export default store
  • 我们可以通过 store.getState 来获取当前的 state;

    js
    import store from './store'
    // 4. 获取store中的state
    ;+console.log(store.getState()) // {name: 'Tom', age: 10}

3、通过 action 来修改 state

  • 通过 dispatch 来派发 action;

  • 通常 action 中都会有 type 属性,也可以携带其他的数据;

js
// 5. 修改store
const nameAction = { type: 'change_name', name: '张飞' }
store.dispatch(nameAction)
console.log(store.getState()) // {name: '张飞', age: 10}

4、修改 reducer 中的处理代码

  • 这里一定要记住,reducer 是一个纯函数,不要直接修改 state;

  • 后面我会讲到直接修改 state 带来的问题;

js
/**
 * 通过reducer函数修改state状态
 * @param {Object} state store中目前保存的state
 * @param {Object} action 本次需要更新的action,通过dispatch传入
 * @returns {Object} state State对象
 */
function reducer(state = initialState, action) {
  switch (action.type) {
    // 有数据更新
    case 'change_name':
      return { ...state, name: action.name }
    // 没有数据更新
    default:
      return state
  }
}

5、可以在派发 action 之前,监听 store 的变化:

订阅 store 中的数据

js
// 6. 订阅(监听)store中的数据
store.subscribe(() => {
  console.log('subscribe: ', store.getState()) // {name: '张飞', age: 10}
})

取消订阅

js
// 6. 订阅(监听)store中的数据
const unsubscribe =
  store.subscribe(() => {
    console.log('subscribe: ', store.getState()) // {name: '张飞', age: 10}
  }) +
  // 取消订阅
  unsubscibe()

6.优化: 使用switch 代替 if else

js
  function reducer(state = initialState, action) {
+    switch (action.type) {
      case 'change_name':
        return { ...state, name: action.name }
      default:
        return state
    }
  }

7.优化: 动态生成action

js
// 7. 优化: 动态生成action
function changeAge(age) {
  return {
    type: 'change_age',
    age
  }
}
store.dispatch(changeAge(33)) // => {name: '张飞', age: 33}

**8.优化: 创建独立的 action 文件:actionCreators.js **

创建:

js
import { CHANGE_NAME, CHANGE_AGE } from './constants'

export function changeName(name) {
  return {
    type: CHANGE_NAME,
    name
  }
}

export default function changeAge(age) {
  return {
    type: CHANGE_AGE,
    age
  }
}

引入使用:

js
import { changeAge, changeName } from './store/actionCreators'

store.dispatch(changeName('刘备'))
store.dispatch(changeAge(55))

**9.优化: 创建独立的常量文件:constants.js **

定义常量:

js
export const CHANGE_NAME = 'change_name'
export const CHANGE_AGE = 'change_age'

使用常量:

actionCreator.js

js
+ import { CHANGE_NAME, CHANGE_AGE } from './constants'

export function changeName(name) {
  return {
+    type: CHANGE_NAME,
    name
  }
}

export function changeAge(age) {
  return {
+    type: CHANGE_AGE,
    age
  }
}

reducer.js

js
+ import { CHANGE_NAME, CHANGE_AGE } from './constants'
const initialState = {
  name: 'Tom',
  age: 10
}
export default function reducer(state = initialState, action) {
  switch (action.type) {
+    case CHANGE_NAME:
      return { ...state, name: action.name }
+    case CHANGE_AGE:
      return { ...state, age: action.age }
    default:
      return state
  }
}

10.优化: 创建独立的 reducer 文件:reducer.js

定义 reducer:

js
import { CHANGE_NAME, CHANGE_AGE } from './constants'

const initialState = {
  name: 'Tom',
  age: 10
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_NAME:
      return { ...state, name: action.name }
    case CHANGE_AGE:
      return { ...state, age: action.age }
    default:
      return state
  }
}

使用 reducer:

js
import { createStore } from 'redux'
+ import reducer from './reducer'

+ const store = createStore(reducer)

export default store

Redux 结构划分

如果我们将所有的逻辑代码写到一起,那么当 redux 变得复杂时代码就难以维护。

  • 接下来,我会对代码进行拆分,将 store、reducer、action、constants 拆分成一个个文件。

  • 创建store/index.js文件:

  • 创建store/reducer.js文件:

  • 创建store/actionCreators.js文件:

  • 创建store/constants.js文件:

注意:node 中对 ES6 模块化的支持

  • 目前我使用的 node 版本是 v12.16.1,从 node v13.2.0 开始,node 才对 ES6 模块化提供了支持:

  • node v13.2.0 之前,需要进行如下操作:

    • 在 package.json 中添加属性: "type": "module";

    • 在执行命令中添加如下选项:node --experimental-modules src/index.js;

  • node v13.2.0 之后,只需要进行如下操作:

    • 在 package.json 中添加属性: "type": "module";

注意:导入文件时,需要跟上.js 后缀名;

Redux 的三大原则

单一数据源

  • 整个应用程序的 state 被存储在一颗 object tree 中,并且这个 object tree只存储在一个 store 中

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

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

State 是只读

  • 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何的方式来修改 State:

  • 这样就确保了 View 或网络请求都不能直接修改 state,它们只能通过 action 来描述自己想要如何修改 state;

  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;

使用纯函数来执行修改

  • 通过 reducer 将 旧 state 和 actions 联系在一起,并且返回一个新的 State:

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

  • 但是所有的 reducer 都应该是纯函数,不能产生任何的副作用

Redux 使用流程

我们已经知道了 redux 的基本使用过程,那么我们就更加清晰来认识一下 redux 在实际开发中的流程:

image-20230404111713118

Redux 官方图

image-20230404111723533

React 结合 Redux

redux 融入 react 代码

目前 redux 在 react 中使用是最多的,所以我们需要将之前编写的 redux 代码,融入到 react 当中去。

这里我创建了两个组件:

  • Home 组件:其中会展示当前的 counter 值,并且有一个+1 和+5 的按钮;

  • Profile 组件:其中会展示当前的 counter 值,并且有一个-1 和-5 的按钮;

image-20230414133811515

核心代码主要是两个:

  • 在 componentDidMount 中订阅数据的变化,当数据发生变化时重新设置 counter;

  • 在发生点击事件时,调用 store 的 dispatch 来派发对应的 action;

示例

1、页面布局

image-20230410175855615

js
import './App.css'
import Home from './cpns/Home'
import Profile from './cpns/Profile'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <h3>App Counter: 0</h3>
        <div className="main">
          + <Home />
          + <Profile />
        </div>
      </div>
    )
  }
}
export default App

2、安装 redux

sh
npm i redux

3、创建 Store

store/index.js

js
import { createStore } from 'redux'
import counterReducer from './reducer'

const store = createStore(counterReducer)

export default store

store/reducer.js

js
import { ADD_COUNTER, SUB_COUNTER } from './constants'

const initialState = {
  counter: 100
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case ADD_COUNTER:
+      return { ...state, counter: state.counter + action.counter }
    case SUB_COUNTER:
+      return { ...state, counter: state.counter - action.counter }
    default:
      return state
  }
}

3、展示 Store 数据

js
export class App extends PureComponent {
  constructor() {
    super()
    this.state = {
+      counter: store.getState().counter
    }
  }
  componentDidMount() {
+    store.subscribe(() => {
+      this.setState({ counter: store.getState().counter })
+    })
  }
  render() {
    const { counter } = this.state
    return (
      <div>
+        <h3>App Counter: {counter}</h3>
        <div className="main">
          <Home />
          <Profile />
        </div>
      </div>
    )
  }
}

export default App

4、修改 Store 数据

constants.js

js
export const ADD_COUNTER = 'add_counter'
export const SUB_COUNTER = 'sub_counter'

actionCreators.js

js
import { ADD_COUNTER, SUB_COUNTER } from './constants'

+ export function addCounter(counter) {
  return {
    type: ADD_COUNTER,
    counter
  }
}
+ export function subCounter(counter) {
  return {
    type: SUB_COUNTER,
    counter
  }
}

调用 action

js
  render() {
    const { counter } = this.state
    return (
      <div>
        <div>Home Counter: {counter}</div>
+        <button onClick={e => this.addCounter(1)}> +1 </button>
+        <button onClick={e => this.addCounter(10)}> +10 </button>
+        <button onClick={e => this.addCounter(100)}> +100 </button>
      </div>
    )
  }

  addCounter(counter) {
+    store.dispatch(addCounter(counter))
  }

react-redux 使用

开始之前需要强调一下,redux 和 react 没有直接的关系,你完全可以在 React, Angular, Ember, jQuery, or vanilla JavaScript 中使用 Redux。

尽管这样说,redux 依然是和 React 库结合的更好,因为他们是通过 state 函数来描述界面的状态,Redux 可以发射状态的更新,让他们作出响应。

虽然我们之前已经实现了connectProvider这些帮助我们完成连接 redux、react 的辅助工具,但是实际上 redux 官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

使用步骤

1、安装 react-redux:

sh
npm i react-redux
# 或者
yarn add react-redux

2、在全局中为整个项目提供 store

js
import { Provider } from 'react-redux'
import store from './05-react-redux使用/store'

const root = ReactDom.createRoot(document.querySelector('#root'))
root.render(
  +(
    <Provider store={store}>
      + <App />+{' '}
    </Provider>
  )
)

3、使用connect 连接 store 和组件

js
  import React, { PureComponent } from 'react'
  import { connect } from 'react-redux'

  import { addCounter } from '../store/actionCreator'

  export class Home extends PureComponent {
    render() {
      // 3. 解构props,获取state和action
+      const { counter, addCounter } = this.props
      return (
        <div>
          {/* 4. 展示state */}
+          <div>Home Counter: {counter}</div>
          {/* 5. 修改state */}
+          <button onClick={e => addCounter(1)}> +1 </button>
+          <button onClick={e => addCounter(10)}> +10 </button>
+          <button onClick={e => addCounter(100)}> +100 </button>
        </div>
      )
    }
  }

  // 2. 定义映射函数,映射state和action到组件的props上
+  const mapStateToProps = state => ({
    counter: state.counter,
  });
+  const mapDispatchToProps = dispatch => ({
    addCounter(counter) {
+      dispatch(addCounter(counter));
    },
  });

  // 1. 使用connect函数连接Home组件和store
+  export default connect(mapStateToProps, mapDispatchToProps)(Home)

react-redux 源码导读

image-20230404111905366

Redux 异步操作

组件中异步操作

在之前简单的案例中,redux 中保存的 counter 是一个本地定义的数据

  • 我们可以直接通过同步的操作来 dispatch action,state 就会被立即更新。

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

在之前学习网络请求的时候我们讲过,网络请求可以在 class 组件的 componentDidMount 中发送,所以我们可以有这样的结构:

image-20230404111934833

我现在完成如下案例操作:

  • 在 Home 组件中请求 banners 和 recommends 的数据;

  • 在 Profile 组件中展示 banners 和 recommends 的数据;

示例

image-20230414152218255

1、 发送异步网络请求,并派送 action,修改 state

js
import { connect } from 'react-redux'

import { getHomeMultidataAction } from '../store/actionCreator'

export class Home extends PureComponent {
  componentDidMount() {
    // 1. 发送异步网络请求
+    axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      this.props.getHomeMultidata({ banners, recommends })
    })
  }
  render() {
    return (
      <div>Home</div>
    )
  }
}

// 2. 派送action,修改state
+ const mapDispatchToProps = dispatch => ({
  getHomeMultidata(data) {
    dispatch(getHomeMultidataAction(data))
  }
})

export default connect(null, mapDispatchToProps)(Home)

2、生成 action 对象

js
// data格式: { banners: [], recommends: [] }
export function getHomeMultidataAction(data) {
  return {
    type: GET_HOMEMULTIDATA,
    data
  }
}

3、修改 reducer,实现修改 state

js
const initialState = {
  banners: [],
  recommends: []
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
+    case GET_HOMEMULTIDATA:
+      return { ...state, banners: action.data.banners, recommends: action.data.recommends }
    default:
      return state
  }
}

4、渲染 state 中的数据到组件中

js
  import { connect } from 'react-redux'

  export class Profile extends PureComponent {
    render() {
+      const { banners, recommends } = this.props
      return (
        <div>
          <div>Profile</div>
          <hr />
          <h3>轮播图列表</h3>
          <ul>
            {
+              banners.map(item => {
+                return (
+                  <li key={item.acm}>{item.title}</li>
+                )
+              })
            }
          </ul>
          <hr />
          <h3>推荐列表</h3>
          <ul>
            {
+              recommends.map(item => (<li key={item.acm}>{item.title}</li>))
            }
          </ul>
        </div>
      )
    }
  }

+  const mapStateToProps = state => ({
+    banners: state.banners,
+    recommends: state.recommends
+  })

+  export default connect(mapStateToProps)(Profile)

redux 中异步操作

上面的代码有一个缺陷

  • 我们必须将网络请求的异步代码放到组件的生命周期中来完成;

  • 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给 redux 来管理;

image-20230404111953021

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

  • 答案就是使用中间件(Middleware)

  • 学习过 Express 或 Koa 框架的童鞋对中间件的概念一定不陌生;

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

理解中间件

redux 也引入了中间件(Middleware)的概念:

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

  • 比如日志记录调用异步接口添加代码调试功能等等;

我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:

  • 这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;

redux-thunk是如何做到让我们可以发送异步的请求呢?

  • 我们知道,默认情况下的 dispatch(action),action 需要是一个 JavaScript 的对象;

  • redux-thunk 可以让 dispatch(action 函数),action 可以是一个函数;

  • 该函数会被调用,并且会传给这个函数一个 dispatch 函数和 getState 函数;

    • dispatch 函数用于我们之后再次派发 action;

    • getState 函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

如何使用 redux-thunk

1、安装 redux-thunk

sh
npm i redux-thunk
# 或者
yarn add redux-thunk

2、在创建 store 时传入应用了 middleware 的 enhance 函数

  • 通过 applyMiddleware 来结合多个 Middleware, 返回一个 enhancer;

  • 将 enhancer 作为第二个参数传入到 createStore 中;

js
+  import { createStore, applyMiddleware } from "redux";
+  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './reducer'

+  const store = createStore(counterReducer, applyMiddleware(thunkMiddleWare))

  export default store

3、定义返回一个函数的 action:

  • 注意:这里不是返回一个对象了,而是一个函数;

  • 该函数在 dispatch 之后会被执行;

js
  // data格式: { banners: [], recommends: [] }
  export function getHomeMultidataAction(data) {
    return {
      type: GET_HOMEMULTIDATA,
      data
    }
  }

  // 发送网络请求
+  export function fetchHomeMultidataAction(data) {
+    return dispatch => {
      axios.get('http://123.207.32.32:8000/home/multidata').then(res => {
        const banners = res.data.data.banner.list
        const recommends = res.data.data.recommend.list
+        dispatch(getHomeMultidataAction({ banners, recommends }))
      })
    }
  }

4、在组件中发起异步请求

js
  export class Detail extends PureComponent {
    componentDidMount() {
+      this.props.fetchHomeMultidata()
    }
    render() {
      const { banners, recommends } = this.props
      ...省略
    }
  }
  const mapStateToProps = state => ({
    banners: state.banners,
    recommends: state.recommends
  })
  const mapDisPatchToProps = dispatch => ({
    fetchHomeMultidata() {
+      dispatch(fetchHomeMultidataAction())
    }
  })

  export default connect(mapStateToProps, mapDisPatchToProps)(Detail)

redux-devtools

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

  • redux 官网为我们提供了 redux-devtools 的工具;

  • 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;

安装该工具需要两步:

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

  • 第二步:在 redux 中继承 devtools 的中间件;

js
  import { createStore, applyMiddleware, compose } from "redux";
  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './reducer'

  // 配置redux-devtools
+  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
+  const store = createStore(counterReducer, composeEnhancer(applyMiddleware(thunkMiddleWare)))

  export default store

开启 trace:

window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true})

js
  import { createStore, applyMiddleware, compose } from "redux";
  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './reducer'

  // 配置redux-devtools
+  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose
  const store = createStore(counterReducer, composeEnhancer(applyMiddleware(thunkMiddleWare)))

  export default store

Reducer 模块拆分

Reducer 代码拆分

我们先来理解一下,为什么这个函数叫 reducer?

我们来看一下目前我们的 reducer:

  • 当前这个 reducer 既有处理 counter 的代码,又有处理 home 页面的数据;

  • 后续 counter 相关的状态或 home 相关的状态会进一步变得更加复杂;

  • 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;

  • 如果将所有的状态都放到一个 reducer 中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。

因此,我们可以对 reducer 进行拆分:

  • 我们先抽取一个对 counter 处理的 reducer;

  • 再抽取一个对 home 处理的 reducer;

  • 将它们合并起来;

Reducer 文件拆分

目前我们已经将不同的状态处理拆分到不同的 reducer 中,我们来思考:

  • 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;

  • 另外关于 reducer 中用到的 constant、action 等我们也依然是在同一个文件中;

image-20230404112214180

1、home/constants.js

js
export const GET_HOMEMULTIDATA = 'get_home_multidata'

2、home/actionCreator.js

js
import axios from 'axios'
import { GET_HOMEMULTIDATA } from './constants'

// data格式: { banners: [], recommends: [] }
export function getHomeMultidataAction(data) {
  console.log(data)
  return {
    type: GET_HOMEMULTIDATA,
    data
  }
}

// 发送网络请求
export function fetchHomeMultidataAction(data) {
  return (dispatch) => {
    axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      dispatch(getHomeMultidataAction({ banners, recommends }))
    })
  }
}

3、home/reducer.js

js
import { GET_HOMEMULTIDATA } from './constants'

const initialState = {
  banners: [],
  recommends: []
}

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case GET_HOMEMULTIDATA:
      console.log('reducer: ', state)
      return { ...state, banners: action.data.banners, recommends: action.data.recommends }
    default:
      return state
  }
}

4、home/index.js

js
import reducer from './reducer'
export default reducer

export * from './actionCreator'

combineReducers 函数

目前我们合并的方式是通过每次调用 reducer 函数自己来返回一个新的对象。

事实上,redux 给我们提供了一个combineReducers函数可以方便的让我们对多个 reducer 进行合并

js
  import { createStore, applyMiddleware, compose, combineReducers } from "redux";
  import thunkMiddleWare from 'redux-thunk'
  import counterReducer from './counter'
  import homeReducer from './home'

  // 合并reducer
+  const reducer = combineReducers({
+    counter: counterReducer,
+    home: homeReducer
+  })

  // 配置redux-devtools
  const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose
  const store = createStore(reducer, composeEnhancer(applyMiddleware(thunkMiddleWare)))

  export default store

注意: 此时的 store 结构类似如下:

js
{
  counter: {
      counter: 100
  },
  home: {
      banners: [],
  	  recommends: []
  }
}

取值时要注意添加 counterhome

那么 combineReducers 是如何实现的呢?

  • 事实上,它也是将我们传入的 reducers 合并到一个对象中,最终返回一个 combination 的函数(相当于我们之前的 reducer 函数了);

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

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

底层原理:combineReducers

image-20230411163117735

ReduxToolkit

RTK-介绍

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

  • 在前面我们学习 Redux 的时候应该已经发现,redux 的编写逻辑过于的繁琐和麻烦。

  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);

  • Redux Toolkit 包旨在成为编写 Redux 逻辑的标准方式,从而解决上面提到的问题;

  • 在很多地方为了称呼方便,也将之称为“RTK”;

RTK-安装

sh
npm install @reduxjs/toolkit react-redux

API

  • configureStore({ reducer, middleware, devTools, ... })返回:store,包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。
    • 参数
    • reducer:``,Redux store 的根 reducer
    • middleware:``,要使用的中间件数组
    • devTools:``,是否启用开发工具(如 Redux DevTools),默认 true
    • preloadedState:``,初始状态
    • enhancers:``,其他 store 增强器
    • 返回
    • store:``,返回的是一个 Redux store 实例,而不是一个类。因此无法创建多个 store 实例
  • createSlice({ name, initialState, reducers,... })返回:reducerSlice,用于创建一个 Redux reducer 和 action creator 的集合
    • 参数
    • nameString,用于标识这个 reducer 的名称,action.type会根据 name 生成
    • initialStateany,表示这个 reducer 的初始状态
    • reducers{ reducer,... },用于定义这个 reducer 的 action creator 和对应的 reducer 函数
      • reducer(state, action) => void,相当于之前的 reducer 函数
    • 返回
    • reducerSlice:``,返回一个 reducer 片段
  • createAsyncThunk(typePrefix, payloadCreator, options? )返回:,用于创建一个异步 action creator
    • 参数
    • typePrefixString,用于标识这个异步 action creator 的类型前缀
    • payloadCreator(arg, thunkAPI) => Promise,用于处理异步操作并返回一个 Promise 对象
    • options?Object,用于配置异步 action creator 的一些选项
      • fulfilled:用于指定异步操作成功时的处理函数。
      • rejected:用于指定异步操作失败时的处理函数。
      • pending:用于指定异步操作进行中时的处理函数。
      • dispatchCondition:用于指定在什么条件下才会 dispatch 这个 action 的函数。
      • condition:用于指定在什么条件下才会调用payloadCreator函数的函数。
      • typeSuffixes:用于指定异步 action creator 的类型后缀的对象。
      • serializeError:用于指定如何序列化异步操作的错误信息的函数。

RTK-基本使用

1、创建 store:configureStore()

store/index.js

js
  import { configureStore } from '@reduxjs/toolkit'
  import counterReducer from './features/counter'
  import homeReducer from './features/home'

+  const store = configureStore({
+    reducer: {
+      counter: counterReducer,
+      home: homeReducer
+    }
+  })

  export default store

2、创建 reducer 片段:createSlice()

couterReducer

js
  import { createSlice } from "@reduxjs/toolkit";

+  const counterSlice = createSlice({
+    name: 'counter',
+    initialState: {
+      counter: 100
+    },
+    reducers: {
+      addCounter(state, action) {
+
+      },
+      subCounter(state, action) {
+
+      }
+    }
+  })

+  export default counterSlice.reducer

3、结合 redux 和 react 组件

  1. 提供 store 到 App 组件

index.js

js
import ReactDom from 'react-dom/client'
import App from './09-reduxToolkit/App'

+ import { Provider } from 'react-redux'
+ import store from './09-reduxToolkit/store'


const root =  ReactDom.createRoot(document.querySelector('#root'))
root.render(
+  <Provider store={store}>
    <App/>
+  </Provider>
)
  1. 获取 store 中的数据

App.jsx

js
  import Home from './cpns/Home'
  import Profile from './cpns/Profile'
  import { connect } from 'react-redux'

  export class App extends PureComponent {
    render() {
+      const { counter } = this.props
      return (
        <div>
+          <h3>App Counter: {counter}</h3>
          <div className="pages">
            <Home />
            <Profile />
          </div>
        </div>
      )
    }
  }

+  const mapStateToProps = state => ({
+    counter: state.counter.counter
+  })

+  export default connect(mapStateToProps)(App)
  1. 修改 store 中的数据

    1. 导出 actions

      js
        import { createSlice } from "@reduxjs/toolkit";
      
        const counterSlice = createSlice({
          name: 'counter',
          initialState: {
            counter: 100
          },
          reducers: {
      +      addCounterAction(state, { payload }) {
              state.counter += payload
            },
      +      subCounterAction(state, { payload }) {
              state.counter -= payload
            }
          }
        })
      
      +  export const { addCounterAction, subCounterAction } = counterSlice.actions
        export default counterSlice.reducer
    2. dispatch 导出的 actions

      js
        import { addCounterAction } from '../store/features/counter'
      
        export class Home extends PureComponent {
          render() {
      +      const { counter, addCounter } = this.props
            return (
              <div>
                <div>Home Counter: {counter}</div>
      +          <button onClick={e => addCounter(1)}> +1 </button>
      +          <button onClick={e => addCounter(10)}> +10 </button>
      +          <button onClick={e => addCounter(100)}> +100 </button>
              </div>
            )
          }
        }
        const mapDispatchToProps = dispatch => ({
      +    addCounter(num) {
      +      dispatch(addCounterAction(num))
      +    }
        })
        export default connect(mapStateToProps, mapDispatchToProps)(Home)

RTK-异步操作

在之前的开发中,我们通过 redux-thunk 中间件让 dispatch 中可以进行异步操作。

Redux Toolkit 默认已经给我们集成了 Thunk 相关的功能createAsyncThunk

js
import { createAsyncThunk } from '@reduxjs/toolkit'

export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
  const res = await axios.get('http://123.207.32.32:8000/home/multidata')
  return res.data.data
})

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

  • pending:action 被发出,但是还没有最终的结果;

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

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

我们可以在 createSlice 的 extraReducer 中监听这些结果:

image-20230404112528739

示例:异步发送网络请求

1、创建 homeSlice

js
// store/features/home.js

  import { createSlice } from "@reduxjs/toolkit";

+  const homeSlice = createSlice({
+    name: 'home',
+    initialState: {
      banners: [],
      recommends: []
    }
  })

+  export default homeSlice.reducer

2、添加片段到 store

js
  import { configureStore } from '@reduxjs/toolkit'

  import counterReducer from '../store/features/counter'
+  import homeReducer from '../store/features/home'

+  const store = configureStore({
+    reducer: {
      counter: counterReducer,
+      home: homeReducer
    }
  })

  export default store

3、展示 home 数据

js
  export class Profile extends PureComponent {
    render() {
+      const { banners, recommends } = this.props
      return (
        <div>
          <h3>Profile</h3>
          <div>发送异步请求</div>
          <div className='lists'>
            <div>
              <h4>轮播图列表</h4>
              <ul>
                {
+                  banners.map((item, index) => {
                    return <li key={item.acm}>{item.title}</li>
                  })
                }
              </ul>
            </div>
            <div>
              <h4>推荐列表</h4>
              <ul>
                {
+                  recommends.map((item, index) => {
                    return <li key={item.acm}>{item.title}</li>
                  })
                }
              </ul>
            </div>
          </div>

        </div>
      )
    }
  }

+  const mapStateToProps = state => ({
    banners: state.home.banners,
    recommends: state.home.recommends
  })

  export default connect(mapStateToProps, mapDispatchToProps)(Profile)

4.1、在组件中发送网络请求(不推荐)

image-20230414180259036

4.2、在 redux 中发送网络请求(推荐

在 store 中定义 fetchHomeMultidataAction,发送网络请求

js
  import axios from 'axios'

  import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

+  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
+    const res = await axios.get('http://123.207.32.32:8000/home/multidata')
+    return res.data.data
+  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
    extraReducers: {
+      [fetchHomeMultidataAction.pending](state, { payload }) {
        console.log('fetchHomeMultidataAction.pending')
      },
+      [fetchHomeMultidataAction.fulfilled](state, { payload }) {
+        console.log(payload)
+        state.banners = payload.banner.list
+        state.recommends = payload.recommend.list
+      },
+      [fetchHomeMultidataAction.rejected](state, { payload }) {
        console.log('fetchHomeMultidataAction.rejected')
      }
    }
    }
  })

  export default homeSlice.reducer

在组件中调用 fetchHomeMultidataAction

js
import { fetchHomeMultidataAction } from '../store/features/home'

  export class Profile extends PureComponent {
+    componentDidMount() {
+      this.props.getHomeMultidata()
+    }
    render() {
+      const { banners, recommends } = this.props
      return (【...省略】)
    }
  }

  const mapStateToProps = state => ({
    banners: state.home.banners,
    recommends: state.home.recommends
  })
  const mapDispatchToProps = dispatch => ({
    getHomeMultidata() {
+      dispatch(fetchHomeMultidataAction())
    }
  })

  export default connect(mapStateToProps, mapDispatchToProps)(Profile)

extraReducer 另二种写法

写法 2: extraReducer 还可以传入一个函数,函数接受一个 builder 参数。

  • 我们可以向 builder 中添加 case 来监听异步操作的结果:
js
+  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async () => {
    const res = await axios.get('http://123.207.32.32:8000/home/multidata')
    return res.data.data
  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
+    extraReducers(builder) {
+      builder
+        .addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
          state.banners = payload.banner.list
          state.recommends = payload.recommend.list
        })
+        .addCase(fetchHomeMultidataAction.rejected, (state) => {
          console.log('fetchHomeMultidataAction.rejected')
        })
    }
  })

写法 3: 直接在 createAsyncThunk 中 dispatch

js
  import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

  export const fetchHomeMultidataAction = createAsyncThunk('home/multidata', async (payload, { dispatch }) => {
    const res = await axios.get('http://123.207.32.32:8000/home/multidata')

    const banners = res.data.data.banner.list
    const recommends = res.data.data.recommend.list
    // 直接在此处dispatch reducers中的action,dispatch是通过参数传递过来的
+    dispatch(setBannersAction(banners))
+    dispatch(setRecommendsAction(recommends))
  })

  const homeSlice = createSlice({
    name: 'home',
    initialState: {
      banners: [],
      recommends: []
    },
    reducers: {
+      setBannersAction(state, { payload }) {
        state.banners = payload
      },
+      setRecommendsAction(state, { payload }) {
        state.recommends = payload
      }
    }
  })

在组件中发送异步请求

js
const mapDispatchToProps = (dispatch) => ({
  getHomeMultidata() {
    ;+dispatch(fetchHomeMultidataAction())
  }
})

export default connect(mapStateToProps, mapDispatchToProps)(Profile)

注意: 如果想要捕获 createAsyncThunk 中的 rejected 状态,需要在回调中使用try...catch包裹发送异步请求的代码

RTK-数据不可变性原理

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

  • 无论是类组件中的 state,还是 redux 中管理的 state;

  • 事实上在整个 JavaScript 编码过程中,数据的不可变性都是非常重要的;

React 中实现数据不可变性的方法有以下几种:

方法一:浅拷贝

  • 展开运算符:``,可以用于数组、对象等数据结构的浅拷贝,避免直接修改原始数据。
  • Array.prototype.concat():``,可以将多个数组合并成一个新的数组,也可以用于数组的浅拷贝。
  • Object.assign():``,可以将多个对象合并成一个新的对象,也可以用于对象的浅拷贝。

浅拷贝的缺点

  • 过大的对象,进行浅拷贝也会造成性能的浪费

  • 浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响

方法二:使用不可变数据结构

  • Immutable.js:``,
  • Immer.js:``,

使用 Immutable.js 或者 Immer.js 等第三方库,这些库提供了一些不可变的数据结构,如 List、Map、Set 等,可以方便地进行数据的不可变性操作。

事实上 Redux Toolkit 底层使用了immerjs的一个库来保证数据的不可变性。

在我们公众号的一片文章中也有专门讲解 immutable-js 库的底层原理和使用方法:

https://mp.weixin.qq.com/s/hfeCDCcodBCGS5GpedxCGg

image-20240719173023257

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

  • 用一种数据结构来保存数据;

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

connect 高阶组件

自定义 connect 函数

手写 connect 高阶函数

1、基础功能:展示 state

js
import { PureComponent } from 'react'

import store from '../store'

// 手写高阶组件connect
function mrConnect(mapStateToProps, mapDispatchToProps) {
  return function (Cpn) {
    return class extends PureComponent {
      render() {
        return <Cpn {...this.props} {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)} />
      }
    }
  }
}

export default mrConnect

2、订阅 store:监听 store 数据变化,重新渲染组件

js
import { PureComponent } from "react"

import store from '../store'

// 手写高阶组件connect
function mrConnect(mapStateToProps, mapDispatchToProps) {
  return function(Cpn) {
    return class extends PureComponent {
      constructor(props) {
        super(props)
+        this.state = mapStateToProps(store.getState())
      }
      componentDidMount() {
+        this.unsubscribe = store.subscribe(() => {
+          this.setState(mapStateToProps(store.getState()))
+        })
      }
      componentWillUnmount() {
+        this.unsubscribe()
      }
      render() {
        return <Cpn {...this.props} {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)} />
      }
    }
  }
}

export default mrConnect

3、使用 connect 高阶函数

js
const mapStateToProps = state => ({
  counter: state.counter.counter
})
const mapDispatchToProps = dispatch => ({
  addCounter(num) {
    dispatch(addCounterAction(num))
  },
  subCounter(num) {
    dispatch(subCounterAction(num))
  }
})

+ export default mrConnect(mapStateToProps, mapDispatchToProps)(App)

context 处理 store

但是上面的 connect 函数有一个很大的缺陷:依赖导入的 store

  • 如果我们将其封装成一个独立的库,需要依赖用于创建的 store,我们应该如何去获取呢?

  • 难道让用户来修改我们的源码吗?不太现实;

正确的做法是我们提供一个 Provider,Provider 来自于我们创建的 Context,让用户将 store 传入到 value 中即可;

4、解耦 store

4.1、创建 StoreContext

js
// hoc\storeContext.js
import { createContext } from 'react'

const StoreContext = createContext()

export default StoreContext

4.2、提供 store 到根元素 App 上

js
// index.js

root.render(
  <StoreContext.Provider value={store}>
    <App />
  </StoreContext.Provider>
)

4.3、在高阶组件mrConnect 中使用 StoreContext 传递的 store

js
  import { PureComponent } from "react"

+  import StoreContext from "./storeContext"

  // 手写高阶组件connect
  function mrConnect(mapStateToProps, mapDispatchToProps) {
    return function(Cpn) {
      class newCpn extends PureComponent {
+        constructor(props, context) {
          super(props)
          this.state = {
+            storeState: context.getState()
          }
        }
        componentDidMount() {
+          this.unsubscribe = this.context.subscribe(() => {
            this.setState({ storeState: this.context.getState() })
          })
        }
        componentWillUnmount() {
          this.unsubscribe()
        }
        render() {
+          return <Cpn {...this.props} {...mapStateToProps(this.context.getState())} {...mapDispatchToProps(this.context.dispatch)} />
        }
      }
+      newCpn.contextType = StoreContext

      return newCpn
    }
  }

  export default mrConnect

中间件的实现原理

打印日志需求

前面我们已经提过,中间件的目的是在 redux 中插入一些自己的操作:

  • 比如我们现在有一个需求在 dispatch 之前,打印一下本次的 action 对象dispatch 完成之后可以打印一下最新的 store state

  • 也就是我们需要将对应的代码插入到 redux 的某部分,让之后所有的 dispatch 都可以包含这样的操作;

方法一:在派发的前后进行相关的打印

如果没有中间件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印。

但是这种方式缺陷非常明显:

  • 首先,每一次的 dispatch 操作,我们都需要在前面加上这样的逻辑代码;

  • 其次,存在大量重复的代码,会非常麻烦和臃肿;

是否有一种更优雅的方式来处理这样的相同逻辑呢?

方法二:将代码封装到一个独立的函数中

  • 我们可以将代码封装到一个独立的函数中

但是这样的代码有一个非常大的缺陷:

  • 调用者(使用者)在使用我的 dispatch 时,必须使用我另外封装的一个函数 dispatchAndLog;

  • 显然,对于调用者来说,很难记住这样的 API,更加习惯的方式是直接调用 dispatch;

修改 dispatch

事实上,我们可以利用一个 hack 一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;

我们对代码进行如下的修改:

  • 这样就意味着我们已经直接修改了 dispatch 的调用过程;

  • 在调用 dispatch 的过程中,真正调用的函数其实是 dispatchAndLog;

当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对 store 进行这样的处理:

store/index.js

js
// 拦截每次的dispatch,打印消息
function log(store) {
  // 1. 保存原始dispatch函数到变量中
  const next = store.dispatch
  // 2. dispatch函数的增强
  function dispatchAndLog(action) {
    console.log('action: ', action)
    next(action)
    console.log('state: ', store.getState())
  }
  // 3. 给store.dispatch重新赋值一个函数
  store.dispatch = dispatchAndLog
}
log(store)

thunk 需求

redux-thunk 的作用:

  • 我们知道 redux 中利用一个中间件 redux-thunk 可以让我们的 dispatch 不再只是处理对象,并且可以处理函数;

  • 那么 redux-thunk 中的基本实现过程是怎么样的呢?事实上非常的简单。

我们来看下面的代码:

  • 我们又对 dispatch 进行转换,这个 dispatch 会判断传入的
js
// 手写中间件:mrThunk
function mrThunk(store) {
  const next = store.dispatch

  function dispatchAndThunk(action) {
    if (typeof action === 'function') {
      //此处传递store.dispatch这个新的dispatch,防止在dispatch的函数中又dispatch了一个
      action(store.dispatch, store.getState)
    } else if (typeof action === 'object') {
      next(action)
    }
  }

  store.dispatch = dispatchAndThunk
}
mrThunk(store)

store 中返回的 action 如下:

js
// 发送网络请求
export function fetchHomeMultidataAction(data) {
  return (dispatch) => {
    axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {
      const banners = res.data.data.banner.list
      const recommends = res.data.data.recommend.list
      dispatch(getHomeMultidataAction({ banners, recommends }))
    })
  }
}

合并中间件

单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:

js
// 手写合并中间件:mrApplyMiddleware
function mrApplyMiddleware(store, ...middlewares) {
  middlewares.forEach((middleware) => {
    middleware(store)
  })
}
mrApplyMiddleware(store, mrLog, mrThunk)

我们来理解一下上面操作之后,代码的流程:

image-20230415111805477

当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考 redux 合并中间件的源码流程。

React 状态管理选择

React 中的 state 如何管理

我们学习了 Redux 用来管理我们的应用状态,并且非常好用(当然,你学会前提下,没有学会,好好回顾一下)。

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

  • 方式一:组件中自己的state管理;

  • 方式二:Context数据的共享状态;

  • 方式三:Redux管理应用状态;

在开发中如何选择呢?

  • 首先,这个没有一个标准的答案;

  • 某些用户,选择将所有的状态放到 redux 中进行管理,因为这样方便追踪和共享;

  • 有些用户,选择将某些组件自己的状态放到组件内部进行管理;

  • 有些用户,将类似于主题、用户信息等数据放到 Context 中进行共享和管理;

  • 做一个开发者,到底选择怎样的状态管理方式,是你的工作之一,可以一个最好的平衡方式(Find a balance that works for you, and go with it.);

Redux 的作者有给出自己的建议:

image-20230404112914790

目前项目中我采用的 state 管理方案:

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

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

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

当然,根据不同的情况会进行适当的调整,在后续学习项目实战时,我也会再次讲解以实战的角度来设计数据的管理方案。