理解纯函数
函数式编程中有一个非常重要的概念叫纯函数,在react开发中纯函数是被多次提及的,所以掌握纯函数对于理解很多框架的设计是非常有帮助的
-
若一个函数符合以下条件,那么这个函数被称为纯函数:
-
此函数在相同的输入值时,需产生相同的输出
-
函数的输出和输入值以外的其他隐藏信息或状态无关 ,也和由
I/O设备产生的外部输出无关 -
函数在执行过程中不能产生副作用,诸如触发事件,使输出设备输出,或更改输出值以外物件的内容等
副作用的理解:表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量、修改参数或者改变外部的存储
-
-
案例:
-
slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组,slice就是一个纯函数 -
React中就要求无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改
-
在接下来学习
redux中,reducer也被要求是一个纯函数
-
为什么需要Redux
-
JavaScript开发的应用程序变得越来越复杂了,需要管理的状态越来越多 -
这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些
UI的状态,比如某些元素是否被选中,是否显示 加载动效,当前分页 -
管理不断变化的
state是非常困难的 ,状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化 -
当应用程序复杂时,
state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪 -
React是在视图层帮助解决了DOM的渲染过程,但是State依然是留给自己来管理 ,无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享 -
Redux就是一个帮助管理State的容器 :Redux是JavaScript的状态容器,提供了可预测的状态管理 -
Redux除了和React一起使用之外,它也可以和其他界面库一起来使用 (比如Vue),并且它非常小(包括依赖在内,只有2kb)
核心理念
Redux 的核心概念围绕状态管理展开,旨在使应用的状态变化可预测和易于调试
store
State 是应用的全部数据,存储在一个单一的 JavaScript 对象中
-
特点 :只读不能直接修改 ,通过
action和reducer更新 -
示例:
jsconst initialState = { user: { name: 'Alice', age: 25 }, todos: [{ id: 1, text: 'Learn Redux', completed: false }] }
action
Action 是一个普通的 JavaScript 对象,用于描述发生了什么(这次更新的type和content)
-
它是更新
State的唯一来源 ,所有数据的变化,必须通过派发(dispatch)action来更新 -
结构 :必须包含一个
type字段表示action的类型,可以包含其他字段(如payload)传递数据jsconst action = { type: 'ADD_TODO', payload: { id: 2, text: 'Learn React', completed: false } } -
上面的
action是固定的对象,真实应用中会通过函数来定义并导出jsconst changeName1Action = ((name)=> ({type: 'change_name', name})) -
强制使用
action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟踪、可预测的
reducer
如何将state和action联系在一起呢?答案就是reducer,Reducer 是一个纯函数,接收当前 State 和 Action,返回新的 State
-
特点:
-
必须是纯函数(相同的输入始终返回相同的输出,无副作用)
-
不能直接修改
State,而是返回一个新的State对象
-
-
示例:
jsfunction todosReducer(state = initialState, action) { switch (action.type) { case 'ADD_TODO': return [...state, action.payload]; default: return state; } }
三大原则
这三大原则确保了 Redux 的可预测性、可调试性和维护性
-
单一数据源:让整个应用的状态集中管理,避免了多处状态的不一致
-
Redux的状态树(state tree)是单一的,整个应用的状态都存储在一个对象树中,这个状态树就是整个应用的单一数据源 -
Redux并没有强制不能创建多个Store,但是那样做并不利于数据的维护 -
单一的数据源可以让整个应用程序的
state变得方便维护、追踪、修改 -
例如应用的所有
UI、用户信息、设置等都在一个全局的state中维护
-
-
状态是只读的:防止直接修改状态,确保所有的状态变化都通过标准化的流程进行
-
Redux中的state不能直接修改,而是只能通过派发(dispatch)行为来间接修改 -
修改
state的唯一途径是触发一个action,并通过一个reducer来计算新的state -
这确保了状态更新的可追溯性,并避免了由于直接修改
state而引发的难以调试的错误
-
-
改变状态只通过纯函数:通过纯函数来避免副作用,确保状态变化是可预测的
-
通过
reducer函数来改变state,reducer是一个纯函数,即给定相同的输入它总是返回相同的输出,并且不会修改输入参数 -
每次
state更新时,reducer会根据当前state和action来返回一个新的state对象,而不是直接修改现有的state -
这也意味着
Redux中的state是不可变的(immutable),避免了由于直接修改数据而产生的副作用 -
随着应用程序的复杂度增加,可以将
reducer拆分成多个小的reducers,分别操作不同state tree的一部分
-
单独使用
在redux的基本使用之前先完成下面的准备工作:
-
创建一个新的项目文件夹 :
learn_redux,执行npm init -
安装
redux:npm install redux --save -
创建
src目录 ,并且创建index.js文件 -
修改
package.json可以执行index.js:"scripts": { "start": "node src/index.js" }
基本
-
创建对象初始化数据 :
const initialState = {name: "shine", counter: 100} -
创建
renducer纯函数不直接修改state:function renducer(state = initialState, action) {// 写根据action修改state逻辑} -
创建
Store来存储这个state:const store = createStore(reducer, enhancer)-
reducer:一个纯函数,用于定义如何更新state -
enhancer(可选):用于增强store的功能,例如添加中间件或开发者工具,后面学习
-
-
可以通过
store.getState来获取当前的state -
可以在派发
action之前,监听store的变化 :const unsubscribe = store.subscribe(()=> {console.log("订阅数据的变化:", store.getState())}) -
可以通过
dispatch来派发action修改state:store.dispatch({type: 'change_name', name: 'changeName2'}) -
可以取消订阅:
unsubscribe(),后面在dispatch就监听不到了 -
练习代码如下:
jsconst { createStore } = require("redux") // 初始化数据 const initialState = { name: "shine", counter: 100 } // 定义reducer函数: 纯函数 // 两个参数: // 参数一: store中目前保存的state // 参数二: 本次需要更新的action(dispatch传入的action) // 返回值: 它的返回值会作为store之后存储的state function renducer(state = initialState, action) { // console.log('renducer', state, action); switch (action.type) { case 'change_name': return {...state, name: action.name} case 'change_counter': return {...state, counter: state.counter + action.num} default: return state // 返回值会作为新的state值存到store中 } } // 创建Store来存储这个state const store = createStore(renducer) // 订阅store中数据的变化,当数据变化时会执行回调并返回一个函数,调用就是取消订阅, store.getState()可以拿到store数据 const unsubscribe = store.subscribe(()=> { console.log("订阅数据的变化:", store.getState()) }) const changeName1Action = ((name)=> ({type: 'change_name', name})) // 修改store中的数据,只能通过action修改 store.dispatch(changeName1Action('changeName1')) // 派发action,会给到reducer的参数action store.dispatch({type: 'change_name', name: 'changeName2'}) unsubscribe() // 取消订阅后,这里监听不到 store.dispatch({type: 'change_counter', num: 5})
优化
上面使用将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护,我们需要对结构进行优化并对代码进行拆分,将store、reducer、action、constants拆分成一个个文件
-
优化思路如下:
-
将派发的
action生成过程放到一个actionCreators函数中 -
将定义的所有
actionCreators的函数, 放到一个独立的文件中:actionCreators.js -
actionCreators和reducer函数中使用字符串常量是一致的,所以将常量抽取到一个独立constants的文件中 -
将
reducer和默认值(initialState)放到一个独立的reducer.js文件中, 而不是在index.js
-
-
创建
storeHoc/constants.js文件:jsconst CHANGE_NAME = 'change_name' const CHANGE_COUNTER = 'change_counter' module.exports = { CHANGE_NAME, CHANGE_COUNTER } -
创建
storeHoc/reducer.js文件:jsconst { CHANGE_NAME, CHANGE_COUNTER } = require('./constants') const initialState = { name: 'shine', counter: 100 } function reducer(state = initialState, action) { switch (action.type) { case CHANGE_NAME: return {...state, name: action.name} case CHANGE_COUNTER: return {...state, counter: state.counter + action.num} default: return state } } module.exports = reducer -
创建
storeHoc/actionCreators.js文件:jsconst { CHANGE_NAME, CHANGE_COUNTER } = require('./constants') const changeNameAction = (name => ({ type: CHANGE_NAME, name })) const changeCounterAction = (num => ({ type: CHANGE_COUNTER, num })) module.exports = { changeNameAction, changeCounterAction } -
创建
storeHoc/index.js文件:jsconst reducer = require('./reducer') const { createStore } = require('redux') const storeHoc = createStore(reducer) module.exports = storeHoc -
index.js中使用优化后的代码修改store:jsconst storeHoc = require("./storeHoc"); const { changeNameAction, changeCounterAction } = require('./storeHoc/actionCreators') const unsubscribeHoc = storeHoc.subscribe(()=>{ console.log('优化后订阅数据的变化', storeHoc.getState()); }) storeHoc.dispatch(changeNameAction('优化name1')) storeHoc.dispatch(changeNameAction('优化name2')) storeHoc.dispatch(changeCounterAction(150)) unsubscribeHoc()
融入react
目前redux在react中使用是最多的,所以需要将上面编写的redux代码,融入到react当中去,在融入到过程中我们会学的很多的知识点,在这里新搭建一个项目
-
脚手架搭建项目 :
create-react-app learn_react_redux -
删除不必要文件,建立新文件 ,整体目录为下图:
-
创建
component文件夹 -
创建
store文件夹,安装redux后面使用npm i redux -
创建
craco.config.js文件配置less,步骤参考juejin.cn/post/747424...jsconst CracoLessPlugin = require("craco-less"); module.exports = { plugins: [ { plugin: CracoLessPlugin, options: { lessLoaderOptions: { lessOptions: { modifyVars: { "@primary-color": "#1DA57A" }, javascriptEnabled: true, }, }, }, }, ], }; -
文件夹
hoc和storeRTK可以后面学习再创建
-
-
接下来将
redux融入react实现下面案例: -
创建
store/constants.js文件:jsexport const ADD_NUMBER = 'add_number' export const SUB_NUMBER = 'sub_number' -
创建
store/reducer.js文件:jsimport * as actionType from './constants' const initialState = { counter: 100, } function reducer(state=initialState, action) { switch (action.type) { case actionType.ADD_NUMBER: return {...state, counter: state.counter + action.num} case actionType.SUB_NUMBER: return {...state, counter: state.counter - action.num} default: return {...state} } } export default reducer -
创建
store/actionCreators.js文件:jsimport * as actionType from './constants' export const addNumber = (num)=>({type: actionType.ADD_NUMBER, num}) export const subNumber = (num)=>({type: actionType.SUB_NUMBER, num}) -
创建
store/index.js文件:jsimport { createStore } from "redux" import reducer from './reducer' const store = createStore(reducer) // 以前可以在这里监听和派发,但现在我们要不这些操作放到组件中 // store.subscribe(() => { console.log(store.getState()) }) // store.dispatch(addAction(10)) // store.dispatch(subAction(5)) export default store -
创建
component/Lift.jsx文件做加法操作:jsimport React, { PureComponent } from 'react' import store from '../store' import { addNumber } from '../store/actionCreators' export class Lift extends PureComponent { constructor() { super() this.state = { counter: store.getState().counter } } componentDidMount() { store.subscribe(()=> { this.setState({ counter: store.getState().counter }) }) } addCounter(num) { store.dispatch(addNumber(num)) } render() { return ( <div> <h3>counter: {this.state.counter}</h3> <button onClick={e=>this.addCounter(1)}>+1</button> <button onClick={e=>this.addCounter(5)}>+5</button> <button onClick={e=>this.addCounter(10)}>+10</button> </div> ) } } export default Lift -
创建
component/Right.jsx文件做减法操作:jsimport React, { PureComponent } from 'react' import store from '../store' import { subNumber } from '../store/actionCreators' export class Right extends PureComponent { constructor() { super() this.state = { counter: store.getState().counter } } componentDidMount() { store.subscribe(() => { const state = store.getState() this.setState({ counter: state.counter }) }) } componentWillUnmount() { } subCounter(num) { store.dispatch(subNumber(num)) } render() { return ( <div> <h3>counter: {this.state.counter}</h3> <button onClick={e=>this.subCounter(11)}>-11</button> <button onClick={e=>this.subCounter(55)}>-55</button> <button onClick={e=>this.subCounter(100)}>-100</button> </div> ) } } export default Right -
app文件的处理 :可以先参考下面代码,都是后面要学习的知识js// index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( // <React.StrictMode> // 严格模式下接口会调用两次,这里注释掉 <App /> // </React.StrictMode> ); // app.jsx import { Provider } from 'react-redux'; import store from './store'; import storeRTK from './storeRTK'; import React, { PureComponent } from 'react' import { AppWrapper } from './style/app'; import Lift from "./component/Lift"; import Right from "./component/Right"; import LiftConnect from './component/LiftConnect'; import RightConnect from './component/RightConnect'; import LiftHttp from './component/LiftHttp'; import RightThunk from './component/RightThunk'; import LiftModule from './component/LiftModule'; import RightModule from './component/RightModule'; import LiftToolkit from './component/LiftToolkit'; import RightToolkit from './component/RightToolkit'; import MyConnect from './component/MyConnect'; import { StoreContext } from './hoc/StoreContext'; export class App extends PureComponent { render() { return ( <AppWrapper> <div className='top'> <h3>react融入redux</h3> </div> <div className='content'> <Lift /> <Right /> </div> {/* Provider联合connect使用 */} <Provider store={store}> <div className='top'> <h3>react融入react-redux使用connect</h3> </div> <div className='content'> <LiftConnect /> <RightConnect /> </div> <div className='top'> <h3>react异步请求和中间件Thunk</h3> </div> <div className='content'> <LiftHttp /> <RightThunk /> </div> <div className='top'> <h3>react Module模块拆分</h3> </div> <div className='content'> <LiftModule /> <RightModule /> </div> </Provider> <Provider store={storeRTK}> <div className='top'> <h3>react 使用工具@reduxjs/toolkit</h3> </div> <div className='content'> <LiftToolkit /> <RightToolkit /> </div> </Provider> <Provider store={store}> <div className='top'> <h3>react 自己实现connect</h3> </div> <div className='content'> <StoreContext.Provider value={store}> <MyConnect /> </StoreContext.Provider> </div> </Provider> </AppWrapper> ) } } export default App // style/app.js const { styled } = require("styled-components"); export const AppWrapper = styled.div` display: flex; flex-direction: column; .top { width: 100%; padding: 0 14px; margin: 14px 0; border: 1px solid orange; } .content { display: flex; width: 100%; > * { flex: 1; padding: 14px; border: 1px solid pink; } } ` -
redux在开发中工作的流程如下图:
react-redux
在上面案例将redux简单融入了react,虽然功能上可以实现状态管理和更新,但还有很多缺点需要我们解决:
-
直接依赖
store,耦合性高 :-
组件直接导入依赖
Redux的store,导致组件与Redux强耦合 -
如果未来需要替换状态管理工具(如使用 MobX 或 Context API),需要修改所有直接依赖
store的组件,组件难以复用
-
-
手动订阅和更新状态,代码冗余 :
-
使用
store.subscribe手动订阅状态变化,并在回调中调用setState更新组件状态 -
代码冗余,每个组件都需要写类似的订阅逻辑,容易遗漏取消订阅的逻辑,可能导致内存泄漏
-
-
性能问题 :
- 每次
store状态变化时,都会触发setState,即使组件依赖的状态没有变化,可能导致不必要的重新渲染
- 每次
-
状态更新逻辑分散 :
- 状态更新逻辑分散在组件的
constructor、componentDidMount和addCounter方法中,可读性和可维护性差,难以追踪状态更新逻辑
- 状态更新逻辑分散在组件的
-
缺乏类型安全 :
- 直接操作
store.getState()和store.dispatch,缺乏类型检查和提示
- 直接操作
使用
react-redux解决了上面的缺点,主要知识点如下,还有其他知识点后面会一一学习:
-
Provider组件 :用于将Redux的store注入到React应用中,使所有子组件都能访问store -
connect函数 :用于将React组件与Redux的store连接起来,使组件能够访问状态和派发actions-
connect(mapStateToProps, mapDispatchToProps)(Component) -
mapStateToProps:将state映射到props -
mapDispatchToProps:将dispatch映射到props -
Component:需要连接的React组件
-
-
useSelector:用于从store中获取状态,函数组件推荐jsimport { useSelector } from 'react-redux'; function Counter() { const counter = useSelector((state) => state.counter); return <div>Counter: {counter}</div>; }-
const stateValue = useSelector(selectorFunction, equalityFunction) -
selectorFunction:一个函数,接收整个Redux state并返回需要的部分状态 -
equalityFunction(可选):用于比较前后状态的函数,默认使用严格相等(===) -
每次
state变化时,useSelector都会重新执行 ,如果返回的值没有变化,组件不会重新渲染,如果需要提取多个状态,可以调用多次useSelector -
如果
useSelector返回的对象或数组是新的引用(即使内容没有变化),组件也会重新渲染。可以使用shallowEqual进行比较优化jsimport { useSelector, shallowEqual } from 'react-redux'; const { counter, user } = useSelector((state) => ({ counter: state.counter, user: state.user, }), shallowEqual);
-
-
useDispatch:dispatch函数是稳定的,不会随组件渲染而变化,因此可以安全地用于useEffect或其他Hooks中jsimport { useDispatch } from 'react-redux'; function Counter() { const dispatch = useDispatch(); const increment = () => dispatch({ type: 'INCREMENT' }); return ( <div> <button onClick={increment}>Increment</button> </div> ); } -
优化
Lift和Right的新组件如下 :app.jsx中需要先用<Provider store={store}></Provider>包裹下面两个组件,使所有子组件都能访问storejs// 创建component/LiftConnect.jsx文件如下 import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { addNumber } from '../store/actionCreators' export class LiftConnect extends PureComponent { render() { return ( <div> <h3>counter: {this.props.number}</h3> <button onClick={e=>this.addNumberConnect(18)}>+18</button> <button onClick={e=>this.addNumberConnect(58)}>+58</button> <button onClick={e=>this.addNumberConnect(108)}>+108</button> </div> ) } addNumberConnect(num) { this.props.addNumberConnect(num) } } /* 1. 使得 store 对于我们的应用是可见的。使用 React Redux 提供的 API <Provider /> 去包裹我们的应用 index.js 2. mapStateToProps将拿到的store数据存到props中,展示可以通过props去拿 s. mapDispatchToProps派发函数存到props中,展示可以通过props去执行action函数 */ const mapStateToProps = state=> ({ number: state.counter }) const mapDispatchToProps = dispatch=> ({ addNumberConnect(num){ dispatch(addNumber(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(LiftConnect) // 创建component/RightConnect.jsx文件如下 import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { subNumber } from '../store/actionCreators' export class RightConnect extends PureComponent { render() { return ( <div> <div> <h3>counter: {this.props.number}</h3> <button onClick={e=>this.subNumberConnect(18)}>-18</button> <button onClick={e=>this.subNumberConnect(58)}>-58</button> <button onClick={e=>this.subNumberConnect(108)}>-108</button> </div> </div> ) } subNumberConnect(num) { this.props.subNumberConnect(num) } } const mapStateToProps = state=>({ number: state.noModule.counter }) const mapDispatchToProps = dispatch=>({ subNumberConnect(num){ dispatch(subNumber(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(RightConnect)
异步操作
-
在前面案例中,
redux中保存的counter是一个本地定义的数据,可以直接通过同步的操作来dispatch action,state就会被立即更新 -
但是真实开发中,
redux中保存的很多数据可能来自服务器,需要进行异步的请求,再将数据保存到redux中 -
网络请求可以在
class组件的componentDidMount中发送,流程如下: -
下载
axios:npm i axios -
store/constants.js中添加常量:jsexport const ADD_NUMBER = 'add_number' export const SUB_NUMBER = 'sub_number' export const CHANGE_BANNERS = 'change_banners' export const CHANGE_RECOMMENDS = 'change_recommends' -
store/actionCreateors.js中添加changeBanners:jsimport axios from 'axios' import * as actionType from './constants' export const addNumber = (num)=>({type: actionType.ADD_NUMBER, num}) export const subNumber = (num)=>({type: actionType.SUB_NUMBER, num}) export const changeBanners = (banners)=>({type: actionType.CHANGE_BANNERS, banners}) export const changeRecommends = (recommends)=>({type: actionType.CHANGE_RECOMMENDS, recommends}) -
store/reducer.js中添加banners:jsimport * as actionType from './constants' const initialState = { counter: 100, banners: [], recommends: [], } function reducer(state=initialState, action) { switch (action.type) { case actionType.ADD_NUMBER: return {...state, counter: state.counter + action.num} case actionType.SUB_NUMBER: return {...state, counter: state.counter - action.num} case actionType.CHANGE_BANNERS: return {...state, banners: action.banners} case actionType.CHANGE_RECOMMENDS: return {...state, recommends: action.recommends} default: return {...state} } } export default reducer -
component/LiftHttp.jsx组件如下:jsimport axios from 'axios' import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { changeBanners } from '../store/actionCreators' export class LiftHttp extends PureComponent { componentDidMount(){ axios.get('http://123.207.32.32:8000/home/multidata').then(res=> { this.props.changeHttpBanners(res.data.data.banner.list) }) } render() { return ( <div> <ul> { this.props.banners.map(item=> { return <li key={item.acm}>{item.title}</li> }) } </ul> </div> ) } } const mapStateToProps = state=> ({ banners: state.banners }) const mapDispatchToProps = dispatch=> ({ changeHttpBanners(banners) { dispatch(changeBanners(banners)) } }) export default connect(mapStateToProps,mapDispatchToProps)(LiftHttp)
redux-thunk
上面的案例代码有一个缺陷,需要将网络请求的异步代码放到组件的生命周期中来完成,事实上网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理,流程如下图:
但是在redux中如何可以进行异步的操作呢?
-
就是使用中间件(
Middleware),学习过Express或Koa框架的应该对中间件的概念不陌生 -
中间件可以帮助我们在请求和响应之间嵌入一些操作的代码,比如
cookie解析、日志记录、文件压缩等操作 -
redux也引入了中间件(Middleware)的概念,目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码,比如日志记录、调用异步接口、添加代码调试功能等等 -
官网推荐的包括演示网络请求的中间件是
redux-thunk
如何使用redux-thunk?
-
安装
redux-thunk:npm i redux-thunk -
在
store/index.js引入thunk:import thunk from "redux-thunk" -
在创建
store时传入应用了中间件的enhance函数:const store = createStore(reducer, applyMiddleware(thunk)) -
把
componentDidMount中的请求放store/actionCreators.js中:jsexport const fetchThunkRecommendsAction = (()=> { return function(dispatch, getState) { axios.get('http://123.207.32.32:8000/home/multidata').then(res=> { dispatch(changeRecommends(res.data.data.recommend.list)) }) } }) -
创建
component/RightThunk.jsx:jsimport React, { PureComponent } from 'react' import { connect } from 'react-redux' import { fetchThunkRecommendsAction } from '../store/actionCreators' export class RightThunk extends PureComponent { componentDidMount() { this.props.fetchThunkRecommends() } render() { return ( <div> <ul> {this.props.recommends.map(item=> { return <li key={item.sort}>{item.title}</li> })} </ul> </div> ) } } const mapStateToProps = state=>({ recommends: state.recommends }) const mapDispatchToProps = dispatch=> ({ fetchThunkRecommends() { dispatch(fetchThunkRecommendsAction()) } }) export default connect(mapStateToProps,mapDispatchToProps)(RightThunk)
redux-devtools
redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
-
redux官网提供了redux-devtools的工具,利用这个工具,可以知道每次状态是如何被修改的,修改前后的状态变化等等 -
安装该工具需要两步:
-
在对应的浏览器中安装相关的插件 (比如
Chrome浏览器扩展商店中搜索Redux DevTools即可) -
在
redux中继承devtools的中间件 ,在store/index.js添加代码:jsconst composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose; const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件
-
拆分模块
在上面的案例中,一个reducer中包含了好几个组件的state数据,以后会越来越臃肿,在实际开发中,项目会有很多个页面,每个页面都需要有自己的reducer,这时候就要拆分模块,这里就不拆分以前的代码了,我们重新写两个组件并拆分reducer来模拟两个页面的拆分
-
创建
store/lift和store/right文件夹,里面放页面用到的actionCreators、constants、reducer、index(主要导出reducer和actionCreators),这里就不说了,文件都和上面例子一样 -
修改
store/index.js文件:jsimport { createStore, combineReducers } from "redux"; import noModuleReducer from "./reducer"; import liftReducer from "./lift"; import rightReducer from "./right"; import thunk from "redux-thunk" // const store = createStore(reducer) const reducer = combineReducers({ liftModule: liftReducer, rightModule: rightReducer, noModule: noModuleReducer, // 以前用到的这个reducer的地方要修改成state.noModule.counter来获取数据 }); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose; const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件 export default store;-
redux给提供了一个combineReducers函数可以方便的让对多个reducer进行合并 -
那么
combineReducers是如何实现的呢?事实上,它也是将传入的
reducers合并到一个对象中,最终返回一个combination的函数在执行
combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state新的
state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新js// combineReducers实现原理(了解) function combineReducers(state = {}, action) { // 返回一个对象, store的state return { counter: counterReducer(state.counter, action), home: homeReducer(state.home, action), user: userReducer(state.user, action) } }
-
-
新建
component/RightModule.jsx文件:jsimport React, { PureComponent } from 'react' import { connect } from 'react-redux' import { fetchRightRecommends } from '../store/right' export class RightModule extends PureComponent { componentDidMount() { this.props.fetchRecommends() } render() { return ( <div> <ul> { this.props.recommends.map(item=> { return <li key={item.sort}>{item.title}</li> }) } </ul> </div> ) } } // 只会找store下面的index文件(取决于你Provider是传入的store引入的那个store/index文件),若此文件中没有合并则无效 const mapStateToProps = state=> ({ recommends: state.rightModule.recommends }) const mapDispatchToProps = dispatch=>({ fetchRecommends() { dispatch(fetchRightRecommends()) } }) export default connect(mapStateToProps,mapDispatchToProps)(RightModule)
Redux Toolkit
-
学习
Redux的时候应该发现,redux的编写逻辑过于的繁琐和麻烦 -
代码通常拆在多个文件中,
Redux Toolkit包旨在成为编写Redux逻辑的标准方式,减少模板代码 -
从而解决上面提到的问题,在很多地方为了称呼方便,也将之称为
RTK -
Redux Toolkit(RTK)内置了Immer,底层使用了ImmutableJS的一个库来保证数据的不可变性,让你可以直接修改state,RTK内部会自动处理不可变性 -
Immer通过Proxy监听state的变化,在produce()结束后,生成新的不可变对象 -
先安装
Redux Toolkit:npm install @reduxjs/toolkit
核心API
Redux Toolkit 让 Redux 开发更加高效,核心API主要有下面几个,实现图中案例来理解一下核心API
-
configureStore:简化store配置,自动集成Redux DevTools和中间件,主要包含如下几个参数:js// 创建storeRTK/index.js import { configureStore } from "@reduxjs/toolkit"; import liftRenducer from "./features/lift"; import rightRenducer from "./features/right"; const store = configureStore({ // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), // 添加中间件 // middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }), // devTools: process.env.NODE_ENV !== "production", // 仅在开发环境启用 Redux DevTools reducer: { lift: liftRenducer, right: rightRenducer, }, }); export default store;-
reducer:将slice中的reducer可以组成一个对象传入此处 -
middleware:可以使用参数,传入其他的中间件getDefaultMiddleware()默认包含redux-thunk、serializableStateInvariant和immutableStateInvariant你可以用
.concat()添加自定义中间件,比如redux-logger,也可以使用.prepend()在默认中间件前添加serializableCheck: false适用于 Redux 状态中包含不可序列化值(如Map、Set、Date) -
devTools:是否配置devTools工具,默认为true,可以手动设置false来关闭它,或根据环境变量控制(如process.env.NODE_ENV)
-
-
createSlice:一步创建reducer和actionjs// 创建storeRTK/features/lift.js import { createSlice } from "@reduxjs/toolkit"; const lifteSlice = createSlice({ name: 'lift', initialState: { counter: 888 }, reducers: { // addCounter(state, action) { // state.counter = state.counter + action.payload // }, addCounter(state, { payload }) { state.counter = state.counter + payload } } }) export const { addCounter } = lifteSlice.actions export default lifteSlice.reducer // 创建component/LiftToolkit.jsx import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { addCounter } from '../storeRTK/features/lift' export class LiftToolkit extends PureComponent { render() { return ( <div> <h3>counter: {this.props.counter}</h3> <button onClick={e=>this.addCounterNum(1)}>+1</button> <button onClick={e=>this.addCounterNum(5)}>+5</button> <button onClick={e=>this.addCounterNum(10)}>+10</button> </div> ) } addCounterNum(num) { this.props.addCounterNum(num) } } const mapStateToProps = state=> ({ counter: state.lift.counter }) const mapDispatchToProps = dispatch => ({ addCounterNum(num) { dispatch(addCounter(num)) } }) export default connect(mapStateToProps, mapDispatchToProps)(LiftToolkit) -
createAsyncThunk:处理异步请求(如API调用),简化redux-thunk-
当
createAsyncThunk创建出来的action被dispatch时,会存在三种状态:pending:action被发出,但是还没有最终的结果fulfilled:获取到最终的结果(有返回值的结果)rejected:执行过程中有错误或者抛出了异常jsimport { createAsyncThunk } from "@reduxjs/toolkit"; export const 异步Action名 = createAsyncThunk( "命名空间/Action名", // 唯一标识,devTools工具会显示 async (参数, thunkAPI) => { try { const response = await fetch("你的API地址"); return await response.json(); // 返回数据 } catch (error) { return thunkAPI.rejectWithValue(error.message); // 失败处理 } } ); -
可以在
createSlice的entraReducer中监听三种状态 ,extraReducer还可以传入一个函数,函数接受一个builder参数,可以向builder中添加case来监听异步操作的结果js// 创建storeRTK/features/right.js import axios from "axios"; const { createSlice, createAsyncThunk } = require("@reduxjs/toolkit"); export const fetchRightBanners = createAsyncThunk( "right/fetchBanners", async (extraInfo, store) => { let { data } = await axios.get("http://123.207.32.32:8000/home/multidata"); /* 修改state.banners的值 1. return 值 ----> [fetchRightBanners.fulfilled](state, action) {赋值} 2. return 值 ----> 通过bulid.addCase(fetchRightBanners.fulfilled,(state, action)=>{赋值} 3. 不return直接使用dispatch修改createAsyncThunk回调函数接受两个参数extraInfo:调用时传的值,store */ // console.log(data, extraInfo, store); store.dispatch(changeBanners(data.data.banner.list)); // return data.data.banner.list // 不return将拿不到值,这里return的值会作为[fetchRightBanners.fulfilled]的action.payloa的值 } ); const rightSlice = createSlice({ name: "right", initialState: { banners: [], }, reducers: { changeBanners(state, { payload }) { state.banners = payload; }, }, /* extraReducers: { [fetchRightBanners.pending](state, action) { console.log(state, action); }, [fetchRightBanners.fulfilled](state, action) { console.log(state, action); state.banners = action.payload }, [fetchRightBanners.rejected](state, action) { console.log(state, action); } } */ /* extraReducers(bulid) { bulid.addCase(fetchRightBanners.pending,(state, action)=>{ console.log('padding', state, action); }).addCase(fetchRightBanners.fulfilled,(state,action)=> { console.log('fulfilled', state, action); }).addCase(fetchRightBanners.rejected,(state, action)=> { console.log('rejected', state, action); }) } */ }); export const { changeBanners } = rightSlice.actions; export default rightSlice.reducer; // 创建component/RightToolkit.jsx import React, { PureComponent } from 'react' import { connect } from 'react-redux' import { fetchRightBanners } from '../storeRTK/features/right' export class RightToolkit extends PureComponent { componentDidMount() { this.props.fetchBanners() } render() { return ( <div> <ul> { this.props.banners.map(item=> { return <li key={item.acm}>{item.title}</li> }) } </ul> </div> ) } } const mapStateToProps = state=> ({ banners: state.right.banners }) const mapDispatchToProps = dispatch=> ({ fetchBanners() { dispatch(fetchRightBanners()) } }) export default connect(mapStateToProps, mapDispatchToProps)(RightToolkit)
-
原理学习
redux的很多知识基本已经学完,下面是一些有利于我们更好的理解redux的练习
connect实现
-
connect是React-Redux提供的高阶组件(HOC),用于将Redux状态和action绑定到React组件,使组件能够访问Redux store并派发action -
虽然
Redux Toolkit (RTK)推荐使用useSelector和useDispatch代替connect,但在某些类组件或旧项目中,connect仍然广泛使用 -
理解
connect特性:-
connect(...)返回一个高阶组件(HOC) :connect(mapStateToProps, mapDispatchToProps)(组件) -
connect通过context获取storeconnect不会直接接收store,而是依赖 React-Redux 提供的context只有组件在
Provider作用域下,connect才能找到store -
connect订阅store更新connect内部使用store.subscribe()监听Reduxstate变化,并重新渲染组件
-
-
理解透彻后那么我们来自己实现
connectjs// 创建hoc/StoreContext.js import { PureComponent } from "react" // 解耦store // import store from '../store' import { StoreContext } from './StoreContext' export function connect(mapStateToProps,mapDispathToProps) { return function(OriginWrapper){ class NewWrapper extends PureComponent{ constructor(props, context) { super(props) this.state = mapStateToProps(context.getState()) } componentDidMount() { this.unsubscribe = this.context.subscribe(()=> { this.setState(mapStateToProps(this.context.getState())) }) } componentWillUnmount() { this.unsubscribe() } render() { // mapStateToProps,mapDispathToProps传进来的都是函数,需要调用并传入参数,才能拿到函数返回值 const stateObj = mapStateToProps(this.context.getState()) const dispatchObj = mapDispathToProps(this.context.dispatch) return <OriginWrapper {...this.props} {...stateObj} {...dispatchObj} /> } } NewWrapper.contextType = StoreContext return NewWrapper } } // 创建hoc/connect.js const { createContext } = require("react"); export const StoreContext = createContext() // 创建component/MyConnect.jsx使用自己写的connect import React, { PureComponent } from 'react' import { fetchLiftBanners } from '../store/lift' import { connect } from '../hoc/connect' export class MyConnect extends PureComponent { componentDidMount() { this.props.fetchBanners() } render() { return ( <div> <ul> { this.props.banners.map(item=> { return <li key={item.acm}>{item.title}</li> }) } </ul> </div> ) } } const mapStateToProps = state=> ({ banners: state.liftModule.banners }) const mapDispatchToProps = dispatch=>({ fetchBanners() { dispatch(fetchLiftBanners()) } }) export default connect(mapStateToProps, mapDispatchToProps)(MyConnect) // app.js引用组件 <StoreContext.Provider value={store}> <MyConnect /> </StoreContext.Provider>
中间件实现
中间件的目的是在redux中插入一些自己的操作
-
现在有一个需求在
dispatch之前打印一下本次的action对象,dispatch完成之后可以打印一下最新的store state,如果没有中间件怎么实现呢?js// 创建store/thunk/log.js /* 逻辑反推梳理: store.dispatch(addNumber(num)) 可见store.dispatch是一个函数,参数也是一个函数 addNumber = (num)=>({type: actionType.ADD_NUMBER, num}) 而addNumber也是一个函数返回对象则 store.dispatch(addNumber(num)) = store.dispatch({type: actionType.ADD_NUMBER, num}) 由此可知 store.dispatch = logDispacth logDispacth也必须是一个函数,并且接受了对象参数相当于{type: actionType.ADD_NUMBER, num} 修改时就可以执行store.dispatch穿进去这个参数,并派发前后打印log */ function log(store) { let init = store.dispatch function logDispacth(action){ console.log('派发之前', store.getState()); init(action) console.log('派发之后', store.getState()); } // 我们可以利用一个hack一点的技术:Monkey Patching(猴补丁),利用它可以修改原有的程序逻辑 store.dispatch = logDispacth } export default log -
那么就可以自己实现
thunk中间件js// 创建store/thunk/thunk.js function thunk(store) { let init = store.dispatch function myThunk(action) { if(typeof action == 'function') { action(init, store.getState) }else { init(action) } } store.dispatch = myThunk } export default thunk -
实现合并中间件的函数
js// 创建store/thunk/middleware.js function middleware(store, ...fn) { fn.forEach(f=>{ f(store) }) } export default middleware -
将三个函数全部导出
js// 创建store/thunk/index.js import log from './log' import thunk from './thunk' import middleware from './middleware' export { log, thunk, middleware } -
使用自己封装的中间件
js// store/index.js import { createStore, combineReducers } from "redux" import liftReducer from './lift' import rightReducer from './right' // import thunk from "redux-thunk" import { middleware, log, thunk } from "./thunk" // const store = createStore(reducer) const reducer = combineReducers({ liftModule: liftReducer, rightModule: rightReducer, }) // const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose; // const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))) // 使用中间件 const store = createStore(reducer) // 自己实现派发前后打印日志和中间件 middleware(store, log, thunk) export default store -
当然真实的中间件实现起来肯定会更加的灵活,有兴趣可以参考
redux合并中间件的源码流程
state管理方案
目前已经主要学习了三种状态管理方式:
-
组件中自己的
state管理 -
Context数据的共享状态 -
Redux管理应用状态 -
在开发中如何选择呢?
Redux的作者有给出自己的建议 -
目前自己采用的
state管理方案:-
UI相关的组件内部可以维护的状态,在组件内部自己来维护 -
大部分需要共享的状态,都交给
redux来管理和维护 -
从服务器请求的数据(包括请求的操作),交给
redux来维护
-