# react 使用 redux 状态管理器

763 阅读16分钟

react 使用 redux 状态管理器

学习资料

英文文档redux.js.org/

中文文档www.redux.org.cn/

Github地址github.com/reactjs/red…

在这里插入图片描述

是什么 redux 状态管理器

  • redux 是一个专门用来做状态管理的JS库,他独立与 React。
  • 它可以用在React、Angular、Vue等项目中, 但常被用来与 React 配合使用。

作用: 集中式管理 React 应用中多个组件共享的状态,就像是 Vue 项目中的 Vuex。

什么时候需要使用 redux

  • 某个组件的状态,需要让其他组件可以随时拿到(共享)。
  • 一个组件需要改变另一个组件的状态(通信)。
  • 总体原则:能不用就不用, 如果不用比较吃力才考虑使用。

redux 原理图

在这里插入图片描述

redux 三大核心概念

action

  1. 动作的对象
  2. 包含2个属性
    • type:标识属性, 值为字符串, 唯一, 必要属性
    • data:数据属性, 值类型任意, 可选属性
  3. 例子:{ type: 'ADD_STUDENT',data:{name: '我是ed.',age:18} }

reducer

  1. 用于初始化状态、加工状态。
  2. 加工时,根据旧的 state 和 action , 产生新的 state 的纯函数

store

  1. 将state、action、reducer联系在一起的对象
  2. 如何得到此对象?
    • import {createStore} from 'redux'
    • import reducer from './reducers'
    • const store = createStore(reducer)
  3. 此对象的功能?
    • getState(): 得到state
    • dispatch(action): 分发action, 触发reducer调用, 产生新的state
    • subscribe(listener): 注册监听, 当产生了新的state时, 自动调用

最简单的 redux 案例

接下来我们写一个最简单的 redux 案例。

首先得安装哈!

npm install redux

接下来开始编写案例:

我们有一个页面,求和 操作。页面显示当前求和的值,默认最开始是 0 ,有一个下拉框,选择要加的数,下面有四个按钮,单击“加”数据加选择的数,点击“减”数据减选择的数,单击“和为基数加”当和为基数的时候加选择的数,单击“异步加”延时500毫秒后加选择的数。具体效果就像下面效果图一样。

在这里插入图片描述

好了, 我们先不使用 redux ,使用组件自己的状态 state 进行维护数据,实现一下这个效果。

因为是组件,所以说呢,我们直接创建一个 Count 然后在他自己内部实现一下这个效果。

import React, { Component } from 'react'

export default class Count extends Component {

  state = { count: 0 }

  // 加法
  increment = () => {
    const { value } = this.selectNumber
    const { count } = this.state
    this.setState({ count: count + value * 1 })
  }

  // 减法
  decrement = () => {
    const { value } = this.selectNumber
    const { count } = this.state
    this.setState({ count: count - value * 1 })
  }

  // 奇数再加
  incrementIfOdd = () => {
    const { value } = this.selectNumber
    const { count } = this.state
    if (count % 2 !== 0) {
      this.setState({ count: count + value * 1 })
    }
  }

  // 异步相加
  incrementAsync = () => {
    const { value } = this.selectNumber
    const { count } = this.state
    setTimeout(() => {
      this.setState({ count: count + value * 1 })
    }, 500)
  }

  render() {
    return (
      <div>
        <h1>我是𝒆𝒅.</h1>
        <h1>当前求和为:{this.state.count}</h1>
        <select ref={c => this.selectNumber = c}>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select> &nbsp;&nbsp;
        <button onClick={this.increment}></button>&nbsp;&nbsp;
        <button onClick={this.decrement}></button>&nbsp;&nbsp;
        <button onClick={this.incrementIfOdd}>和为奇数加</button>&nbsp;&nbsp;
        <button onClick={this.incrementAsync}>异步加</button>
      </div>
    )
  }
}

【本部分相关代码资料】:我是𝒆𝒅. 的 gitee

OK哈,上边代码就实现了我们案例的效果,尽管他不是使用的 redux 操作的,所以呢,他自己维护了一个 state 值来进行操作,下面我们就实现一下如何把上面的代码修改成 redex 的。

首先需要创建一个文件夹 redux ,我们把与 redux 相关的代码都放到里面去。顺便创建一个 store.js 文件和一个 count_reducer.js 文件。

store.js

store.js 文件需要做什么呢?看原理图,这个文件我们就用来暴露一个 store 对象,记住一点哈,就是整个 react 项目只有一个 store 对象。这个文件里面的东西呢,很简单:

/**
 * 该文件专门用于暴露一个 store 对象,整个应用只有一个 store 对象
 */

// 引入 legacy_createStore 重命名为 createStore 专门用于创建 redux 中最为核心的 store 对象
import { legacy_createStore as createStore } from 'redux'
// 引入为 Count 组件服务的 reducer
import countReducer from './count_reducer'
// 暴露Store
export default createStore(countReducer)

count_reducer.js

count_reducer.js 文件的作用是什么嘞!这个文件是用于创建一个为 Count 组件服务的 reducer,本质就是一个函数 ,而且 reducer 函数会接到两个参数,一个是之前状态 preState,一个是动作对象 action。

然后所有根计算相关的代码我们放到这里面处理,其实就两个嘛!一个加操作,一个减操作。异步加和奇数加不要写进里面,怎么理解呢,好比是个饭店,reducer 就是一个厨子,他只负责“做菜”,要不要香菜(异步加),吃不吃辣(只有奇数加)他不管,不要告诉他,他只管最基本的“做”。什么时候处理异步加和奇数加,你自己判断处理。

/**
 * 该文件是用于创建一个为 Count 组件服务的 reducer,本质就是一个函数 
 * reducer 函数会接到两个参数,一个是之前状态 preState,一个是动作对象 action。
 */

const initState = 0

export default function countReducer(preState = initState, action) {
  // 从 action 对象中获取 type 和 data
  const { type, data } = action
  // 根据 type 决定如何加工数据
  switch (type) {
    case 'increment': // 加运算
      return preState + data
    case 'decrement': // 减运算
      return preState - data
    default:
      return preState
  }
}

OK,这样我们最基本的 redux 就写完了,就是用了,在哪里用?我们最开始的案例代码,也就是 Count 组件。

首先我们把 count 数据交给 redux 处理的话,Count 组件中不需要自己维护 state 了,当然如果有其他的可以自己维护,起码 count 不需要自己维护了。

  // 不需要自己家的了,使用 redux 维护的
  // state = { count: 0 }

然后我们写了 store,我们需要把他引入进 Count 组件来才能用。

// 引入 store,用于获取 redux 中获取的状态
import store from '../../redux/store'

最开始我们使用组件自己的状态维护的数据,展示到页面的。

<h1>当前求和为:{this.state.count}</h1>

所以呢,接下来我们不能使用了呀!得用 store 维护的了,怎么展示呢?有一个方法,叫 getState(),通过他,我们就可以获取到 store 状态中维护的数据。

<h1>当前求和为:{store.getState()}</h1>

写到这里,我们页面就可以正常展示数据了,但是呢,不能点击,因为点击按钮还是之前的,已经不行了。

在这里插入图片描述 如何实现点击按钮进行计算呢,redux 原理图说了 store 操作需要使用 dispatch,这里呢,需要传递 action 对象,一个是 type 即执行什么操作,一个是 data 要操作的数据值。比如说加操作:

  // 加法
  increment = () => {
    const { value } = this.selectNumber
    store.dispatch({ type: 'increment', data: value * 1 })  // 实现加,数据是下拉框选择的数据值
  }

OK,编写完,我们点击“加”按钮试一下效果。

在这里插入图片描述

我们点击按钮了之后,发现数据并没有实现 “加” 操作,为啥呢?首先检查 type: 'increment' 有没有单词拼写错误。如果没有,那就是因为,状态确实改变了,但是! BUT! 页面没有更新!

是的,redux 不是 react 的成员,是一个编外人员,他只负责数据状态的维护管理,他不管你最后渲染如何。这里和 setState 不一样,setState 会帮我们修改状态并且调用 render() 重新渲染页面,但是看 redux 的原理图,他只帮我们维护数据,返回计算完成的数据,并没有说帮我们渲染,所以说我们需要自己渲染页面。

怎么渲染呢?也很简单,我们需要用到一个新的方法 subscribe,看下面的代码:

  componentDidMount() {
    // 检测 redux 中状态的变化,只要变化就调用 render()
    store.subscribe(() => {
      // this.setState({})
      this.forceUpdate()
    })
  }

subscribe 他是什么?就是订阅啊,当 store 的任何一个数据变化之后,都会被监听到,然后我们把他写到了组件加载完成的生命周期里,组件一挂载完成就走开始监听,然后呢,当 store 数据变了之后,就自动的刷新页面,要么强制刷新页面 this.forceUpdate(),要么 this.setState({}) 更新状态,更新的啥呢?啥也没更新,就是单纯的更新页面,我们晃他一下子。

在这里插入图片描述 这样子就可以实现点击 “加” 操作了。

同理其他按钮也可以类似实现:

import React, { Component } from 'react'
// 引入 store,用于获取 redux 中获取的状态
import store from '../../redux/store'

export default class Count extends Component {

  // 不需要自己家的了,使用 redux 维护的
  // state = { count: 0 }

  componentDidMount() {
    // 检测 redux 中状态的变化,只要变化就调用 render()
    store.subscribe(() => {
      // this.setState({})
      this.forceUpdate()
    })
  }

  // 加法
  increment = () => {
    const { value } = this.selectNumber
    store.dispatch({ type: 'increment', data: value * 1 })
  }

  // 减法
  decrement = () => {
    const { value } = this.selectNumber
    store.dispatch({ type: 'decrement', data: value * 1 })
  }

  // 奇数再加
  incrementIfOdd = () => {
    const { value } = this.selectNumber
    const count = store.getState()
    if (count % 2 !== 0) {
      store.dispatch({ type: 'increment', data: value * 1 })
    }
  }

  // 异步相加
  incrementAsync = () => {
    const { value } = this.selectNumber
    const count = store.getState()
    setTimeout(() => {
      store.dispatch({ type: 'increment', data: value * 1 })
    }, 500)
  }

  render() {
    return (
      <div>
        <h1>我是𝒆𝒅.</h1>
        <h1>当前求和为:{store.getState()}</h1>
        <select ref={c => this.selectNumber = c}>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select> &nbsp;&nbsp;
        <button onClick={this.increment}></button>&nbsp;&nbsp;
        <button onClick={this.decrement}></button>&nbsp;&nbsp;
        <button onClick={this.incrementIfOdd}>和为奇数加</button>&nbsp;&nbsp;
        <button onClick={this.incrementAsync}>异步加</button>
      </div>
    )
  }
}

但是有一个问题,不算问题,算是拓展吧,我们 Count 组件使用了 store 然后需要刷新页面,万一我们有100个组件都使用了 store ,然后我们每个页面都需要刷新吗?是的,需要。那我们每个页面是不是都得写监听呀?

  componentDidMount() {
    // 检测 redux 中状态的变化,只要变化就调用 render()
    store.subscribe(() => {
      // this.setState({})
      this.forceUpdate()
    })
  }

是的,都要写,但是我们想一下,我们可不可以换一个地方一次性就处理了?嘿嘿,是的,打开 index.js 文件,直接在挂载整个页面的时候监听就可以吧?引入 store ,然后监听。

// 引入 react 核心库
import React from 'react';

// 引入 react-dom 核心库
import ReactDOM from 'react-dom/client';
// 引入 App 组件
import App from './App';
import store from './redux/store';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
    <App />
);

store.subscribe(() => {
    root.render(
        <App />
    );
})

OK,效果是一样的。

在这里插入图片描述

有人会说这样效率会不会降低啊,其实不用太在意,还记得 Diff 算法吗?

总结:

  • 去除Count组件自身的状态
  • src下建立:
-redux
	-store.js
	-count_reducer.js
  • store.js:

    • 引入 redux 中的 createStore 函数,创建一个 store
    • createStore 调用时要传入一个为其服务的 reducer
    • 记得暴露 store 对象
  • count_reducer.js:

    • reducer 的本质是一个函数,接收:preStateaction,返回加工后的状态
    • reducer 有两个作用:初始化状态,加工状态
    • reducer 被第一次调用时,是 store 自动触发的,
      • 传递的 preStateundefined,
      • 传递的 action 是: {type:'@@REDUX/INIT_a.2.b.4}
  • 在 index.js 中监测 store 中状态的改变,一旦发生改变重新渲染 <App/>

    • 备注:redux 只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。

好的今天就到这里,明天继续!

【本部分相关代码资料】:我是𝒆𝒅. 的 gitee

相对完整的 redux 使用

我们上面的案例哈,实现了一个最简单的 redux 的使用,但是看原理图:

在这里插入图片描述

我们上面案例并没有使用 Action,现在呢,我们就把 Action 这一部分给加上去。我们看原理图,Action 的作用是返回一个对象,对象里面包含着 type 和 data 传递给 dispatch,上面案例使我们自己写的对象传递的,直接把 Action 省略掉了,好的,下面编写代码。

使用 Action 首先一点,就是创建吧?首先在 src 文件夹下创建一个 count_action.js 文件,然后呢,这个文件就是专门用来负责为 Count 组件生成 action 对象。

/**
 * 该组件专门为 Count 组件生成 action 对象
 */

// 加操作
export const createIncrementAction = data => ({type: 'increment', data})
// 减操作
export const createDecrementAction = data => ({type: 'decrement', data})

OK,通过 Action 生成 action 对象了,所以说在 Count 组件点击按钮的时候就可以修改一下子了。

比如说加操作,最开始我们是自己创造传递的对象:

  // 加法
  increment = () => {
    const { value } = this.selectNumber
    store.dispatch({ type: 'increment', data: value * 1 })
  }

下面我们可以改成 Action 创建了,首先引入我们刚刚创建的 Action:

// 引入 actionCreator,专门用于创建 action 对象
import { createDecrementAction, createIncrementAction } from '../../redux/count_action'

然后嘞,就是使用了呀!

  // 加法
  increment = () => {
    const { value } = this.selectNumber
    store.dispatch(createIncrementAction(value * 1))
  }

同理,我们也可以对 减、异步加、和为奇数加 进行修改。

  // 减法
  decrement = () => {
    const { value } = this.selectNumber
    store.dispatch(createDecrementAction(value * 1))
  }

  // 奇数再加
  incrementIfOdd = () => {
    const { value } = this.selectNumber
    const count = store.getState()
    if (count % 2 !== 0) {
      store.dispatch(createIncrementAction(value * 1))
    }
  }

  // 异步相加
  incrementAsync = () => {
    const { value } = this.selectNumber
    setTimeout(() => {
      store.dispatch(createIncrementAction(value * 1))
    }, 500)
  }

OK,这样子我们就把 Action 部分也添加进来了,这样三大模块全部使用了。

在这里插入图片描述 其实到这里就可以了,但是呢,真正做项目的时候会发现,人家公司开发的项目,处理这几个 redux 文件之外,还会有一个 constant.js 文件。这个文件里面存放着一些变量,啥变量呢?

在这里插入图片描述 看上边的图,我们在使用 redux 的时候,需要设置这个 type 的,除了 action 在 reducer 也会用到,而且他们之间是需要统一的,但是很多程序员在开发的时候发现开发完成之后呢,没有效果,有一部分检查发现是两边的单词因为手误拼写错误的,所以说我们创建一个变量文件,将用到的变量维护起来,这样后期修改如果需要修改 type 内容可以进行统一修改,在一个呢,就是方式程序员小可爱在开发过程中手误打错单词,导致效果出不来的问题。

我们把 incrementdecrement 放到 constant.js 变量文件里面维护起来。

/**
 * 该模块用于定义 action 对象中的 type 类型的常量
 * 该文件的目的有且只有一个,就是便于管理的同时,防止程序员小可爱在编写代码的时候手急把单词拼写错误。
 */

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

注意哈,变量一般使用全大写声明。

然后我们在需要使用的地方引入一下文件,然后直接替换掉以前的就可以啦!

首先是 Count 组件:

import { createDecrementAction, createIncrementAction } from '../../redux/count_action'

  // 加法
  increment = () => {
    const { value } = this.selectNumber
    store.dispatch(createIncrementAction(value * 1))
  }

  // 减法
  decrement = () => {
    const { value } = this.selectNumber
    store.dispatch(createDecrementAction(value * 1))
  }

  // 奇数再加
  incrementIfOdd = () => {
    const { value } = this.selectNumber
    const count = store.getState()
    if (count % 2 !== 0) {
      store.dispatch(createIncrementAction(value * 1))
    }
  }

  // 异步相加
  incrementAsync = () => {
    const { value } = this.selectNumber
    setTimeout(() => {
      store.dispatch(createIncrementAction(value * 1))
    }, 500)
  }

reducer 文件:

/**
 * 该文件是用于创建一个为 Count 组件服务的 reducer,本质就是一个函数 
 * reducer 函数会接到两个参数,一个是之前状态 preState,一个是动作对象 action。
 */

// 引入常量
import { DECREMENT, INCREMENT } from '../redux/constant'

const initState = 0

export default function countReducer(preState = initState, action) {
  // 从 action 对象中获取 type 和 data
  const { type, data } = action
  // 根据 type 决定如何加工数据
  switch (type) {
    case INCREMENT: // 加运算
      return preState + data
    case DECREMENT: // 减运算
      return preState - data
    default:
      return preState
  }
}

好了,保存刷新看一下效果,是完全一样的。

在这里插入图片描述

【本部分相关代码资料】:我是𝒆𝒅. 的 gitee

异步 Action

上面一直在说,Action 中存放的是一个对象,对象里面两个属性,一个 type,一个 data。好,这是其中一种,其实 Action 还可以放另一种东西,就是 function。

当 Action 中是一个 Object 对象的时候,他就是一个 同步 Action,当是 function 的时候就是一个 异步 Action。

我们上面那个案例有一个 “异步加” 操作,点击完成之后呢,会延时500毫秒之后实现加操作,这是异步Action吗? NO! 这不是滴!这是我们自己在 Count 组件里面实现的吧?不是 Action 帮我们实现的,所以不是 异步Action。

怎么理解呢、前边案例的异步加操作,就好比去饭店吃饭,我们(Count 组件)坐下来先不点餐,先玩半个小时的手机(延时500毫秒),半个小时之后点餐,点完服务员(Action)直接上菜!

但是异步 Action 是啥意思呢?就是我们不操作了,直接让服务员来!

去饭店吃饭,我们(Count 组件)坐下来就点餐,然后告诉服务员(Action)说半个小时后给我上菜,半个小时之后服务员(Action)直接上菜!

OK了吗?就是把异步操作由我们的组件,改为 Action 实现。

好的接下来我们就可以把之前的 异步加 修改为 异步 Action 实现了。

既然是使用异步Action,那么我们组件自己的异步就可以不需要了,而是在 Action 中创建一个 异步加 的 action 调用了吧!

  // 引入 actionCreator,专门用于创建 action 对象
  import { createDecrementAction, createIncrementAction, createIncrementAsyncAction } 		from '../../redux/count_action'

  // 异步相加
  incrementAsync = () => {
    const { value } = this.selectNumber
    // setTimeout(() => {
      store.dispatch(createIncrementAsyncAction(value * 1, 500)) // 传递数据和延时时长
    // }, 500)
  }

然后我们需要去 action 创建一个 createIncrementAsyncAction 方法。

/**
 * 该组件专门为 Count 组件生成 action 对象
 */

// 引入常量
import { DECREMENT, INCREMENT } from '../redux/constant'
// import store from './store'

// 加操作  同步 Action 返回的是一般类型的 Object
export const createIncrementAction = data => ({ type: INCREMENT, data })
// 减操作
export const createDecrementAction = data => ({ type: DECREMENT, data })
// 异步加
export const createIncrementAsyncAction = (data, time) => {
  // 异步 Action 返回的是函数,因为函数里面可以开启异步任务, 异步 Action 中一般都会调用同步 Action。
  return (dispatch) => {  // 回调会有 dispatch ,这样就不用再引用 store 了。
    setTimeout(() => {
      dispatch(createIncrementAction(data)); // 到时间调用自己的同步加操作
      // store.dispatch(createIncrementAction(data)); // 使用 store 需要引入
    }, time)
  }
}

异步Action必须返回的是一个函数,因为只有函数可以开启异步任务。

按照同步的使用方式,到这里应该结束了吧,但是异步的不行,我们可以先看一下效果。

在这里插入图片描述 这是为啥哈,稍微解释一下下,就一下下哈,一点不说多。就是我们这个 Action 是交给 store 来处理的吧?但是 store 这个东西啊是个老古板,他只接受一般类型的 Object ,因为要有 type 和 data 才能交给 reducer 去处理。但是嘞,我们使用的 异步 Action 返回的不是一般类型的 Object 而是一个函数,这时候 store 就不干了!你他妈的这不是找事儿嘛!我不要!

那我们怎么办呢?其实报错的说的很详细了。我们需要一个人,这个人就是中间件,他去告诉 store 说 “好哥哥,通融一下,我不需要你帮忙执行,你就帮我收一下先,到点了我在给你一个真正的一般类型的 Object 去处理。” 然后嘞,store 就帮我们解决异步Action了。

那么这个中间件是啥子嘞!就是 —— redux-thunk

首先我们需要安装一下 redux-thunk

npm i redux-thunk

安装完成了之后去创建中间件告诉 store 啊,所以说呢,我们去修改 store。

/**
 * 该文件专门用于暴露一个 store 对象,整个应用只有一个 store 对象
 */

// 引入 legacy_createStore 重命名为 createStore 专门用于创建 redux 中最为核心的 store 对象
import { legacy_createStore as createStore, applyMiddleware } from 'redux'
// 引入为 Count 组件服务的 reducer
import countReducer from './count_reducer'
// 引入 redux-thunk 用于支持异步 Action
import thunk from 'redux-thunk' 

// 暴露Store  ,这里第二个参数就是为了处理 异步 Action
export default createStore(countReducer, applyMiddleware(thunk))

好的,这样子之后呢,我们异步的 Action 就可以正常使用了!

在这里插入图片描述 好了,这个异步的 Action 使用就解决了。

稍稍总结一番:

  • 明确:延迟的动作不想交给组件自身,想交给action
  • 何时需要异步action:想要对状态进行操作,但是具体的数据靠异步任务返回。
  • 具体编码:
    • yarn add redux-thunk,并配置在store中
    • 创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。
    • 异步任务有结果后,分发一个同步的action去真正操作数据。
  • 备注:异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。

【本部分相关代码资料】:我是𝒆𝒅. 的 gitee

好的,今天就到这里,拜了个白!

在这里插入图片描述