理解纯函数
函数式编程中有一个非常重要的概念叫纯函数,在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>
包裹下面两个组件,使所有子组件都能访问store
js// 创建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
和action
js// 创建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
获取store
connect
不会直接接收store
,而是依赖 React-Redux 提供的context
只有组件在
Provider
作用域下,connect
才能找到store
-
connect
订阅store
更新connect
内部使用store.subscribe()
监听Redux
state
变化,并重新渲染组件
-
-
理解透彻后那么我们来自己实现
connect
js// 创建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
来维护
-