ReactJS语法-redux
redux
JavaScript纯函数
- 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
- 纯函数特点:
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
- 副作用:表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
- 举例:
- slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
- splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;
- slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数;
-
举例2
//该函数就是一个纯函数,1有确定的输入,就能够有确定的输出。2. 函数执行对外部没有任何副作用 function sum(num1,num2){ return num1+num2; } //这个函数就不是一个纯函数,因为引用了外部的变量,外部变量可以随时修改,那么就不能保证确定的输入有确定的输出。 let foo = 5; function add(num1){ return num1+foo; } //不是纯函数,因为函数内部修改了外部的数据,产生了新的副作用 function printInfo(info){ info.name = "hhh"; return info; } - React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改。
为什么需要redux
- JavaScript需要管理的状态越来越多,越来越复杂;这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
- Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
几个核心概念
- Store:定义统一的规范来操作这段数据。
- action
- Redux要求我们通过action来更新数据
- 所有数据的变化,必须通过派发(dispatch)action来更新;
- action是一个普通的JavaScript对象,用来描述这次更新的type和content;
- 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;
- reducer
- 但是如何将state和action联系在一起呢?答案就是reduce
- reducer是一个纯函数;
- reducer做的事情就是将传入的state和action结合起来生成一个新的state;
Redux的三大原则
- 单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
- State是只读的
- 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
- 使用纯函数来执行修改
- 使用纯函数来执行修改
- 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State:
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用
Redux的使用
- 创建一个对象,作为我们要保存的状态:
- 创建Store来存储这个state
- 创建store时必须创建reducer;
- 我们可以通过 store.getState 来获取当前的state;
- 通过action来修改state
- 通过dispatch来派发action;
- 通常action中都会有type属性,也可以携带其他的数据;
- 修改reducer中的处理代码
- 这里一定要记住,reducer是一个纯函数,不需要直接修改state;
- 可以在派发action之前,监听store的变化:
案例
-
创建store/index.js文件:
//这里是nodejs环境,因此使用require引入 const { createStore } = require("redux") //2. 创建store时必须创建reducer; const reducer = require("./reducer.js") //1. 创建的store const store = createStore(reducer) module.exports = store -
创建store/reducer.js文件:
const { ADD_NUMBER, CHANGE_NAME } = require("./constants") // 初始化的数据 const initialState = { name: "why", counter: 100 } //store首次被创建的时候,state没有值,因此可以设置默认值。后序使用state就代表现状的值 function reducer(state = initialState, action) { switch(action.type) { case CHANGE_NAME: //1. 对原有的state进行深拷贝。2. 修改新的state的name值。 3. 返回新的state return { ...state, name: action.name } case ADD_NUMBER: return { ...state, counter: state.counter + action.num } default: return state } } module.exports = reducer -
创建store/actionCreators.js文件:
//封装一个对象,用于快速创建一个action const { ADD_NUMBER, CHANGE_NAME } = require("./constants") const changeNameAction = (name) => ({ type: CHANGE_NAME, //等价于 num : num name }) const addNumberAction = (num) => ({ type: ADD_NUMBER, num }) module.exports = { changeNameAction, addNumberAction } -
创建store/constants.js文件
//常量记录 const ADD_NUMBER = "add_number" const CHANGE_NAME = "change_name" module.exports = { ADD_NUMBER, CHANGE_NAME } -
外部文件使用store
const store = require("./store") const { addNumberAction, changeNameAction } = require("./store/actionCreators") //在派发action之前,监听store的变化: const unsubscribe = store.subscribe(() => { //使用store中的数据 console.log("订阅数据的变化:", store.getState()) }) // 修改store中的数据: 必须action store.dispatch(changeNameAction("kobe")) store.dispatch(addNumberAction(10)) //取消订阅 unsubscribe()
react中使用redux
- redux和react没有直接的关系,你完全可以在React, Angular, Ember, jQuery, JavaScript中使用Redux
- 安装redux:
npm install redux - 举例:
-
App.jsx
import React, { PureComponent } from 'react' import Home from './pages/home' import Profile from './pages/profile' import store from "./store" export class App extends PureComponent { constructor() { super() this.state = { counter: store.getState().counter.counter } } componentDidMount() { store.subscribe(() => { const state = store.getState().counter this.setState({ counter: state.counter }) }) } render() { const { counter } = this.state return ( <div> <h2>App Counter: {counter}</h2> <div className='pages'> <Home/> <Profile/> </div> </div> ) } } export default App -
Home.jsx
import React, { PureComponent } from 'react' import store from "../store" import { addNumberAction } from '../store/actionCreators' export class Home extends PureComponent { constructor() { super() this.state = { counter: store.getState().counter, } } componentDidMount() { store.subscribe(() => { const state = store.getState() this.setState({ counter: state.counter }) }) } addNumber(num) { store.dispatch(addNumberAction(num)) } render() { const { counter } = this.state return ( <div> <h2>Home Counter: {counter}</h2> <div> <button onClick={e => this.addNumber(1)}>+1</button> <button onClick={e => this.addNumber(5)}>+5</button> </div> </div> ) } } export default Home -
Profile.jsx
import React, { PureComponent } from 'react' import store from "../store" import { subNumberAction } from '../store/actionCreators' export class Profile extends PureComponent { constructor() { super() this.state = { counter: store.getState().counter } } componentDidMount() { store.subscribe(() => { const state = store.getState() this.setState({ counter: state.counter }) }) } subNumber(num) { store.dispatch(subNumberAction(num)) } render() { const { counter } = this.state return ( <div> <h2>Profile Counter: {counter}</h2> <div> <button onClick={e => this.subNumber(1)}>-1</button> <button onClick={e => this.subNumber(5)}>-5</button> </div> </div> ) } } export default Profile -
store文件夹下
// index.js import { createStore, applyMiddleware, compose } from "redux" import reducer from "./reducer" const store = createStore(reducer) export default store //reducer.js import * as actionTypes from "./constants" const initialState = { counter: 100 } function reducer(state = initialState, action) { switch (action.type) { case actionTypes.ADD_NUMBER: return { ...state, counter: state.counter + action.num } case actionTypes.SUB_NUMBER: return { ...state, counter: state.counter - action.num } default: return state } } export default reducer //actionCreators.js import * as actionTypes from "./constants" export const addNumberAction = (num) => ({ type: actionTypes.ADD_NUMBER, num }) export const subNumberAction = (num) => ({ type: actionTypes.SUB_NUMBER, num }) -
react-redux使用
- 从上面的案例可以看出,App.jsx、Home.jsx、Profile.jsx如果要使用redux,都必须要:
- constructor中获取store数据对state初始化
- componentDidMount中订阅store数据的变化,然后重新渲染页面
- 操作store对数据数据进行更新
- 那么是否可以将以上操作使用一个高阶组件进行抽取封装起来,应用组件只需要:
- 告知哪些state数据需要被初始化
- 哪些数据需要被更新
- 这个就是react-redux的作用
举例使用:
- 安装: npm install react-redux
-
在入口index.js文件中
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; //1. 引用Provider import { Provider } from "react-redux" //2. 引用store import store from "./store" const root = ReactDOM.createRoot(document.getElementById('root')); root.render( {/*3. 使用Provider组件包裹App组件,并将store作为传参,目的是为了react-redux内部能够拿到store来操作*/} <Provider store={store}> <App /> </Provider> ); -
新增一个About.jsx
import React, { PureComponent } from 'react' //1. 引入connect import { connect } from "react-redux" import { addNumberAction, subNumberAction } from '../store/actionCreators' export class About extends PureComponent { calcNumber(num, isAdd) { if (isAdd) { console.log("加", num) // 3.1. this.props可以拿到addNumber函数用来修改stroe的数据 this.props.addNumber(num) } else { console.log("减", num) this.props.subNumber(num) } } render() { // 3.2. this.props可以拿到counter用来展示数据 const { counter } = this.props return ( <div> <h2>About Page: {counter}</h2> <div> <button onClick={e => this.calcNumber(6, true)}>+6</button> <button onClick={e => this.calcNumber(6, false)}>-6</button> </div> </div> ) } } // connect()返回值是一个高阶组件 // function mapStateToProps(state) { // return { // counter: state.counter // } // } // function fn2(dispatch) { // return { // addNumber(num) { // dispatch(addNumberAction(num)) // }, // subNumber(num) { // dispatch(subNumberAction(num)) // } // } // } const mapStateToProps = (state) => ({ counter: state.counter, banners: state.banners, recommends: state.recommends }) const mapDispatchToProps = (dispatch) => ({ addNumber(num) { dispatch(addNumberAction(num)) }, subNumber(num) { dispatch(subNumberAction(num)) } }) /* 1. 调用connect这个高阶函数 2.1. connect是一个高阶函数,传参为2个函数 2.2. 第一个函数参数:告诉react-redux需要将state的哪个属性进行初始化、监听渲染、传递给当前的About组件的Props属性用来获取。 2.3. 第二个函数参数:告诉react-redux需要修改state的那个属性值,并将这个函数传递给About组件的Props属性用来调用 2.4. connect的返回值是一个高阶组件,传参为组件,返回值是一个新的组件 内部原理------: 通过调用mapStateToProps,拿到当前About组件state那个属性,那么就可以确定store的哪个属性需要被被监听,然后将监听变更后的结果以props的方式传递给About组件 通过调用mapDispatchToProps,可以拿到变更store数据的函数实现,然后赋值给About组件的props,这样About就可以调用自己的props方法来修改store */ export default connect(mapStateToProps, mapDispatchToProps)(About) -
新增一个APP.jsx使用
render() { const { counter } = this.state return ( <div> <h2>App Counter: {counter}</h2> <div className='pages'> <Home/> <Profile/> <About/> </div> </div> ) }
redux存储异步网络请求的数据
-
创建一个组件专门用来获取网络请求的数据Category.jsx
import React, { PureComponent } from 'react' import { connect } from "react-redux" //actionCreators新增一个action方法 import { fetchHomeMultidataAction } from "../store/actionCreators" export class Category extends PureComponent { componentDidMount() { //获取网络请求,并将网络请求数据存储到store this.props.fetchHomeMultidata() } render() { return ( <div> </div> ) } } const mapDispatchToProps = (dispatch) => ({ //网络请求数据方法实现 fetchHomeMultidata() { //修改store,通常dispatch的参数只能是一个action对象,但是这里fetchHomeMultidataAction()返回值是一个函数 dispatch(fetchHomeMultidataAction()) } }) export default connect(null, mapDispatchToProps )(Category) -
actionCreators实现fetchHomeMultidataAction函数
import * as actionTypes from "./constants" import axios from "axios" export const addNumberAction = (num) => ({ type: actionTypes.ADD_NUMBER, num }) export const subNumberAction = (num) => ({ type: actionTypes.SUB_NUMBER, num }) //封装一个acion,直接存放服务器返回的banners数据 export const changeBannersAction = (banners) => ({ type: actionTypes.CHANGE_BANNERS, banners }) /* 1. 这个方法通常返回的是一个action对象 2. 理论上是先获取网络请求的数据,然后将数据封装到aciton中去 3. 但是网络请求时异步的,因此无法直接返回 4. 解决办法:返回一个函数,同时将dispatch、state传参,在这个函数实现里面去网络请求,等网络请求回来之后在进行dispatch修改store数据 5. 为了让action方法支持返回一个函数,需要使用第三方库:redux-thunk,即npm install redux-thunk */ export const fetchHomeMultidataAction = () => { // 如果是一个普通的action, 那么我们这里需要返回action对象 // 问题: 对象中是不能直接拿到从服务器请求的异步数据的 // return {} return function(dispatch, getState) { // 异步操作: 网络请求 // console.log("foo function execution-----", getState().counter) axios.get("http://123.207.32.32:8000/home/multidata").then(res => { const banners = res.data.data.banner.list //实际修改store数据 dispatch(changeBannersAction(banners)) }) } } -
index.js
import { createStore, applyMiddleware } from "redux" import thunk from "redux-thunk" //store通过thunk做了一个增强,在修改store时dispatch可以传递函数,而且一旦传递函数,会直接调用,并传参dispatch, getState const store = createStore(reducer,applyMiddleware(thunk)) export default store
redux-thunk是如何做到让我们可以发送异步的请求
- 默认情况下的dispatch(action),action需要是一个JavaScript的对象;
- redux-thunk可以让dispatch(action函数),action可以是一个函数;
- 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
- dispatch函数用于我们之后再次派发action
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
ReduxToolkit
- Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
- 在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦
- 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理)
- Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;
- 在很多地方为了称呼方便,也将之称为“RTK”;
- 安装Redux Toolkit:
npm install @reduxjs/toolkit react-redux - Redux Toolkit的核心API主要是如下几个
- configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。
- createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。
- createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk
configureStore
- 用于创建store对象,常见参数如下:
- reducer,将slice中的reducer可以组成一个对象传入此处;
- middleware:可以使用参数,传入其他的中间件(自行了解);
- devTools:是否配置devTools工具,默认为true;
createSlice
- 通过createSlice创建一个slice
- createSlice主要包含如下几个参数:
- name:用户标记slice的名词,
- initialState:初始化值,第一次初始化时的值;
- reducers:相当于之前的reducer函数
- 对象类型,并且可以添加很多的函数;
- 函数类似于redux原来reducer中的一个case语句;
- 函数的参数:
- 参数一:state
- 参数二:调用这个action时,传递的action参数;
- createSlice返回值是一个对象,包含所有的actions;
举例
-
项目入口index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import { Provider } from "react-redux" import App from './App'; import store from './store'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> ); -
APP.js
import React, { PureComponent } from 'react' import { connect } from "react-redux" import Profile from './pages/Profile' import Home from './pages/Home' import "./style.css" export class App extends PureComponent { render() { const { counter } = this.props return ( <div> <h2>App Counter: {counter}</h2> <div className='pages'> <Home/> <Profile/> </div> </div> ) } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) export default connect(mapStateToProps)(App) -
store文件夹
//index.js import { configureStore } from "@reduxjs/toolkit" import counterReducer from "./features/counter" const store = configureStore({ reducer: { counter: counterReducer } }) export default store //./features/counter.js import { createSlice } from "@reduxjs/toolkit" const counterSlice = createSlice({ name: "counter", initialState: { counter: 888 }, reducers: { //函数有2个参数,一个是state,一个是aciton,aciton有2个参数一个是type一个是payload,这里直接解构为payload addNumber(state, { payload }) { state.counter = state.counter + payload }, subNumber(state, { payload }) { state.counter = state.counter - payload } } }) export const { addNumber, subNumber } = counterSlice.actions export default counterSlice.reducer -
Profile.jsx
import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { subNumber } from '../store/features/counter' export class Profile extends PureComponent { subNumber(num) { this.props.subNumber(num) } render() { const { counter} = this.props return ( <div> <h2>Page Counter: {counter}</h2> <button onClick={e => this.subNumber(5)}>-5</button> <button onClick={e => this.subNumber(8)}>-8</button> </div> ) } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ subNumber(num) { dispatch(subNumber(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(Profile)
Redux Toolkit的异步操作
- 在之前的开发中,我们通过redux-thunk中间件让dispatch中可以进行异步操作
- Redux Toolkit默认已经给我们继承了Thunk相关的功能:createAsyncThunk
- 当createAsyncThunk创建出来的action被dispatch时,会存在三种状态:
- pending:action被发出,但是还没有最终的结果;
- fulfilled:获取到最终的结果(有返回值的结果)
- rejected:执行过程中有错误或者抛出了异常;
- 可以在createSlice的entraReducer中监听这些结果
举例使用
-
Home.jsx 在该页面进行触发网络请求
import React, { PureComponent } from 'react' import { connect } from "react-redux" import { addNumber } from '../store/features/counter' import { fetchHomeMultidataAction } from '../store/features/home' export class Home extends PureComponent { componentDidMount() { //出发网络请求数据 this.props.fetchHomeMultidata() } addNumber(num) { this.props.addNumber(num) } render() { const { counter } = this.props return ( <div> <h2>Home Counter: {counter}</h2> <button onClick={e => this.addNumber(5)}>+5</button> <button onClick={e => this.addNumber(8)}>+8</button> <button onClick={e => this.addNumber(18)}>+18</button> </div> ) } } const mapStateToProps = (state) => ({ counter: state.counter.counter }) const mapDispatchToProps = (dispatch) => ({ addNumber(num) { dispatch(addNumber(num)) }, fetchHomeMultidata() { dispatch(fetchHomeMultidataAction({name: "why", age: 18})) } }) export default connect(mapStateToProps, mapDispatchToProps)(Home) -
在store中添加对一个的reducer
// store/features/home.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import axios from 'axios' export const fetchHomeMultidataAction = createAsyncThunk( "fetch/homemultidata", async (extraInfo, { dispatch, getState }) => { // console.log(extraInfo, dispatch, getState) // 1.发送网络请求, 获取数据 const res = await axios.get("http://123.207.32.32:8000/home/multidata") return res.data }) const homeSlice = createSlice({ name: "home", initialState: { banners: [], recommends: [] }, reducers: { changeBanners(state, { payload }) { state.banners = payload }, changeRecommends(state, { payload }) { state.recommends = payload } }, //监听createAsyncThunk的返回过程 extraReducers: { [fetchHomeMultidataAction.pending](state, action) { console.log("fetchHomeMultidataAction pending") }, //拿到返回结果修改state [fetchHomeMultidataAction.fulfilled](state, { payload }) { state.banners = payload.data.banner.list state.recommends = payload.data.recommend.list }, [fetchHomeMultidataAction.rejected](state, action) { console.log("fetchHomeMultidataAction rejected") } } }) export const { changeBanners, changeRecommends } = homeSlice.actions export default homeSlice.reducer // 将homeReducer添加给store //store/index.js import { configureStore } from "@reduxjs/toolkit" import counterReducer from "./features/counter" import homeReducer from "./features/home" const store = configureStore({ reducer: { counter: counterReducer, //新增homeReducer home: homeReducer } }) export default store -
Profile.jsx展示数据
import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { subNumber } from '../store/features/counter' export class Profile extends PureComponent { subNumber(num) { this.props.subNumber(num) } render() { const { counter, banners, recommends } = this.props return ( <div> <h2>Page Counter: {counter}</h2> <button onClick={e => this.subNumber(5)}>-5</button> <button onClick={e => this.subNumber(8)}>-8</button> <div className='banner'> <h2>轮播图展示</h2> <ul> { banners.map((item, index) => { return <li key={index}>{item.title}</li> }) } </ul> </div> <div className='recommend'> <h2>推荐的展示</h2> <ul> { recommends.map((item, index) => { return <li key={index}>{item.title}</li> }) } </ul> </div> </div> ) } } const mapStateToProps = (state) => ({ counter: state.counter.counter, banners: state.home.banners, recommends: state.home.recommends }) const mapDispatchToProps = (dispatch) => ({ subNumber(num) { dispatch(subNumber(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(Profile)
React中的state如何管理
- 三种状态管理方式:
- 组件中自己的state管理;
- Context数据的共享状态;
- Redux管理应用状态;
- 项目中采用的state管理方案:
- UI相关的组件内部可以维护的状态,在组件内部自己来维护;
- 大部分需要共享的状态,都交给redux来管理和维护
- 从服务器请求的数据(包括请求的操作),交给redux来维护;