Redux
为什么要使用Redux
- JavaScript开发的应用程序,已经变得越来越复杂了
- JavaScript需要管理的状态越来越多,越来越复杂
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示 加载动效,当前分页
- 管理不断变化的state是非常困难的
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪
- React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理
- 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享
- React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定
- React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定
- Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
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
1
npm install redux --save
创建一个对象,作为我们要保存的状态
创建Store来存储这个state
创建store时必须创建reducer
我们可以通过 store.getState 来获取当前的state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28const { createStore } = require("redux")
// 初始化数据——要保存的状态
const initialState = {
name:"ouwenwu",
age:22
}
// 定义reducer函数:纯函数
/**
* 参数1:store中目前报错的state——在createStore时会调用reducer一次,此时参数state为undefined,因此需要为其定义一个初始值,即我们的初始化数据
* 参数2:传入的action
* 返回值:返回值会作为store之后存储的state
*/
function reducer(state=initialState,action){
console.log("reducer",state,action)
// 这里使用switch更好
switch(action.type){
case "change_name":
return {...state,name:action.name}
case "add_age":
return {...state,age:state.age+action.age}
default:
return state
}
}
//创建store
const store = createStore(reducer)
module.exports = store通过action来修改state
- 通过dispatch来派发action
- 通常action中都会有type属性,也可以携带其他的数据
修改reducer中的处理代码
- 这里一定要记住,reducer是一个纯函数,不需要直接修改state
- 后面我会讲到直接修改state带来的问题
可以在派发action之前,监听store的变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const store=require("./store");
// 开启store订阅——这个订阅是在派发action之前订阅的,会在store所监听的state修改时调用
const unsubscribe = store.subscribe(()=>{
console.log("订阅数据发生变化",store.getState())
})
// actionFunction——将action抽取为函数,那么后面每次需要创建action不用手动编写,调用函数生成
const changeNameAction = (name)=>({
type:"change_name",
name
})
const addAgeAction = (age)=>({
type:"add_age",
age
})
// 修改store中的数据:必须action——手动写action
const nameAction ={type:"change_name",name:"kobe"}
store.dispatch(nameAction)
store.dispatch(changeNameAction("kebi1"))
// 取消订阅
unsubscribe()
store.dispatch({type:"add_age",age:1})
store.dispatch(addAgeAction(1))输出
Redux项目抽取
如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护
store/index.js文件——利用reducer创建store
1
2
3
4import {createStore} from "redux"
import reducer from "./reducer"
//创建store
export const store = createStore(reducer)store/reducer.js文件——reducer函数和state默认值\
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import * as actionType from "./constants.js"
const initialState = {
name:"ouwenwu",
age:22
}
// 定义reducer函数:纯函数
/**
* 参数1:store中目前报错的state
* 参数2:传入的action
* 返回值:返回值会作为store之后存储的state
*/
function reducer(state=initialState,action){
console.log("reducer",state,action)
// 这里使用switch更好
switch(action.type){
case actionType.CHANGE_NAME:
return {...state,name:action.name}
case actionType.ADD_AGE:
return {...state,age:state.age+action.age}
default:
return state
}
}
export default reducerstore/actionCreator.js文件——将store要派发的action,抽取成函数放在此处
1
2
3
4
5
6
7
8
9
10import * as actionType from "./constants.js"
export const changeNameAction = (name)=>({
type:actionType.CHANGE_NAME,
name
})
export const addNameAge = (age)=>({
type:actionType.ADD_AGE,
age
})store/constants.js文件——常量文件,将action中的type抽取出来
1
2export const CHANGE_NAME = "change_name"
export const ADD_AGE = "add_age"
Redux与React融合
redux配置代码如上所述
jsx代码:
1 | import React from "react"; |
react-redux的使用
index.js(整个react的入口):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App"
import { Provider } from "react-redux";
import store from "./store";
// 编写REACT代码,通过REACT渲染内容
/**
* 这里的Provider实际上是基于Context实现的,用它包裹App及其子组件可以使用传入的stero
* stero在底层是通过value实现的
*/
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(
<Provider store={store}>
<App/>
</Provider>
)App.js代码(获取stero并使用代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44import React from "react";
import { connect } from "react-redux";
import { addAgeAction } from "./store/actionCreator";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
addAgeByButton(){
// store.dispatch(addAgeAction(1))
this.props.addAge(1)
}
render(){
return (
<div>
<button onClick={()=>{this.addAgeByButton()}}>age+1</button>
<div>
<h3>{this.props.age}</h3>
</div>
</div>
)
}
}
// 相当于添加监听,会把stero中的state中的对应属性添加到props中
function mapStateToProps(state){
// 需要用哪些就使用哪些,会将这个返回对象和本来的props合并
return{
age:state.age
}
}
// 设置dispatch,会把return中的函数放到this.pros中去,通过调用这个函数可以发送action给stero
const mapDispatchToProps = (dispatch)=>{
return{
addAge(age){
dispatch(addAgeAction(age))
}
}
}
// connect()返回值是一个高阶函数
/**
* connect()参数1:store中的哪些数据需要映射到这个组件的props——函数
* connect()参数2:
*/
export default connect(mapStateToProps,mapDispatchToProps)(App)stero的代码,index.js/constants.js/actionCreate.js/reducer.js依然像上方写的一样
redux异步处理
react获取网络请求:
componentDidMount中发起网络请求,并在结束后赋值给state
- 这种方式数据管理和react耦合性高,和redux的要求不符
通过redux获取网络请求,由jsx组件中发起action,在action中获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 由于网络请求通常是异步的,函数的return中不能直接获得网络请求的值,因此普通的action是不能实现异步的
*/
export const netWorkAction = ()=>{
new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("oww")
},5000)
}).then(res=>{
return{
type:actionType.CHANGE_NAME,
name:res
}
})
return {}
}redux-thunk中间件技术
安装redux-thunk
1
npm install react-thunk --save
设置react可以使用thunk,thunk允许dispatch一个函数,之后会自动执行这个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { applyMiddleware, combineReducers, createStore } from "redux";
import countReducer from "./countStore/reducer";
import homeReducer from "./homeStore/reducer";
import thunk from "redux-thunk";
const reducer = combineReducers({
age:countReducer,
name:homeReducer
})
// combineReducers的底层实现
function reducerByUs(state = {}, action){
return {
/**
* 第一次执行时(createStore),传入undefined,得到的是默认值对象
* 后续执行时,每次传入上次的state,得到正确值
*/
age:countReducer(state.age, action),
name:homeReducer(state.name,action)
}
}
// 包裹中间件
export const store = createStore(reducer,applyMiddleware(thunk))
export default storeaction代码
1
2
3
4
5
6
7
8
9
10
11
12
13export const netWorkAction = ()=>{
console.log("dispatch foo")
const foo = (dispatch, getState)=>{
new Promise((resolve) => {
setTimeout(()=>{
resolve("oww")
},5000)
}).then(res=>{
dispatch(changeNameAction(res))
})
}
return foo
}jsx中发起dispatch代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53import React from "react";
import { connect } from "react-redux";
import { addAgeAction } from "./store/countStore";
import { netWorkAction } from "./store/homeStore";
//编写组件
class App extends React.PureComponent{
constructor(){
super()
}
addAgeByButton(){
// store.dispatch(addAgeAction(1))
this.props.addAge(1)
this.props.changeName()
}
render(){
return (
<div>
<button onClick={()=>{this.addAgeByButton()}}>changeinfo</button>
<div>
<h2>{this.props.name}</h2>
<h3>{this.props.age}</h3>
</div>
</div>
)
}
}
// 相当于添加监听,会把stero中的state中的对应属性添加到props中
function mapStateToProps(state){
// 需要用哪些就使用哪些,会将这个返回对象和本来的props合并
return{
age:state.age.age,
name:state.name.name
}
}
// 设置dispatch,会把return中的函数放到this.pros中去,通过调用这个函数可以发送action给stero
const mapDispatchToProps = (dispatch)=>{
return{
addAge(age){
dispatch(addAgeAction(age))
},
changeName(){
dispatch(netWorkAction())
}
}
}
// connect()返回值是一个高阶函数
/**
* connect()参数1:store中的哪些数据需要映射到这个组件的props——函数
* connect()参数2:
*/
export default connect(mapStateToProps,mapDispatchToProps)(App)
redux代码拆分
不拆分的reducer:
- 一个reducer处理多个页面的数据
- 将所有状态都放到reducer中进行管理,随着项目的日趋庞大,会造成代码臃肿
对reducer进行拆分:
- 将对不同页面的数据或造成抽取为不同的reducer
- 将多个reducer合并为一个
combineReducers函数
事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并
页面一store代码:
actionCreator.js
1
2
3
4
5
6import * as actionType from "./constants.js"
export const changeNameAction = (name)=>({
type:actionType.CHANGE_NAME,
name
})constants.js
1
export const CHANGE_NAME = "change_name"
index.js
1
2
3
4
5
6import homeReducer from "./reducer"
// 统一当初当前模块的数据
export default homeReducer
export * from "./actionCreator"
export * from "./constants"reducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import * as actionType from "./constants.js"
const initialState = {
name:"ouwenwu",
age:22
}
// 定义reducer函数:纯函数
/**
* 参数1:store中目前报错的state
* 参数2:传入的action
* 返回值:返回值会作为store之后存储的state
*/
function homeReducer(state=initialState,action){
console.log("reducer",state,action)
// 这里使用switch更好
switch(action.type){
case actionType.CHANGE_NAME:
return {...state,name:action.name}
default:
return state
}
}
export default homeReducer
页面一store代码:
actionCreator.js
1
2
3
4
5
6import * as actionType from "./constants.js"
export const changeNameAction = (name)=>({
type:actionType.CHANGE_NAME,
name
})constants.js
1
export const CHANGE_NAME = "change_name"
index.js
1
2
3
4
5
6import homeReducer from "./reducer"
// 统一当初当前模块的数据
export default homeReducer
export * from "./actionCreator"
export * from "./constants"reducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import * as actionType from "./constants.js"
const initialState = {
name:"ouwenwu",
age:22
}
// 定义reducer函数:纯函数
/**
* 参数1:store中目前报错的state
* 参数2:传入的action
* 返回值:返回值会作为store之后存储的state
*/
function homeReducer(state=initialState,action){
console.log("reducer",state,action)
// 这里使用switch更好
switch(action.type){
case actionType.CHANGE_NAME:
return {...state,name:action.name}
default:
return state
}
}
export default homeReducer
页面二store代码:
actionCreator.js
1
2
3
4
5
6import * as actionType from "./constants.js"
export const addAgeAction = (age)=>({
type:actionType.ADD_AGE,
age
})constants.js
1
export const ADD_AGE = "add_age"
index.js
1
2
3
4
5import countReducer from "./reducer"
export default countReducer
export * from "./actionCreator"
export * from "./constants"reducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import * as actionType from "./constants.js"
const initialState = {
age:22
}
// 定义reducer函数:纯函数
/**
* 参数1:store中目前报错的state
* 参数2:传入的action
* 返回值:返回值会作为store之后存储的state
*/
function countReducer(state=initialState,action){
console.log("reducer",state,action)
// 这里使用switch更好
switch(action.type){
case actionType.ADD_AGE:
console.log(state.age,action.age)
return {...state,age:state.age+action.age}
default:
return state
}
}
export default countReducer
合并两个store的代码:
index.js:
1 | import { combineReducers, createStore } from "redux"; |
合并数据的使用:
1 | import React from "react"; |
ReduxToolkit的使用(RTK)
安装
1
npm install @reduxjs/toolkit react-redux
Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题
Redux Toolkit的核心API主要是如下几个:
configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension
createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions
- createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分 派动作类型的 thunk
createSlice代码
1 | import { createSlice } from "@reduxjs/toolkit"; |
configureStore代码
1 | import { configureStore } from "@reduxjs/toolkit" |
jsx代码
1 | import React from "react"; |