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
- 动作的对象
- 包含2个属性
- type:标识属性, 值为字符串, 唯一, 必要属性
- data:数据属性, 值类型任意, 可选属性
- 例子:
{ type: 'ADD_STUDENT',data:{name: '我是ed.',age:18} }
reducer
- 用于初始化状态、加工状态。
- 加工时,根据旧的 state 和 action , 产生新的 state 的
纯函数。
store
- 将state、action、reducer联系在一起的对象
- 如何得到此对象?
import {createStore} from 'redux'import reducer from './reducers'const store = createStore(reducer)
- 此对象的功能?
- 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>
<button onClick={this.increment}>加</button>
<button onClick={this.decrement}>减</button>
<button onClick={this.incrementIfOdd}>和为奇数加</button>
<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>
<button onClick={this.increment}>加</button>
<button onClick={this.decrement}>减</button>
<button onClick={this.incrementIfOdd}>和为奇数加</button>
<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对象
- 引入 redux 中的
-
count_reducer.js:
reducer的本质是一个函数,接收:preState,action,返回加工后的状态- reducer 有两个作用:初始化状态,加工状态
- reducer 被第一次调用时,是
store自动触发的,- 传递的
preState是undefined, - 传递的
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 内容可以进行统一修改,在一个呢,就是方式程序员小可爱在开发过程中手误打错单词,导致效果出不来的问题。
我们把 increment 和 decrement 放到 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
好的,今天就到这里,拜了个白!