day-081-eighty-one-20230530-redux工程化-react-redux-高阶组件
redux工程化
- redux工程化就是模块化开发。
工程化步骤
-
redux工程化的第一种方式:拆分和合并reducer。
- 把各个可以状态集群reducer独立放到单个js文件中。
- 使用combineReducers合并拆分出去的reducer,得到一个
新的总reducer
。 - 使用createStore根据
新的总reducer
生成一个store公共状态容器。 - 创建文件
/src/Theme.js
,用于创建上下文对象并在根组件/src/index.jsx
中引入。将公共容器放在根组件的上下文对象中。 - 获取公共容器中的公共状态并使用。
- 设置公共容器中的公共状态。
-
把大的reducer拆分成多个reducer。最后通过combineReducers把各个reducer合成一个总reducer。最后createStore根据总reducer得到公共状态容器。
state = {vote:{//...},task:{//...} } store.getState()//得到类似于上方的东西。所以还要取具体模块,再解析出一个公共状态的具体公共状态。
- 每一次派发,会把所有的小的reducer都通知执行一遍。
- 每个小的reducer中只能难道此模块下的状态,修改的也是此模块下的状态。
- 每一次派发,会把所有的小的reducer都通知执行一遍。
-
上方就是redux工程化的第一种方式:reducer的拆分和合并。
-
redux工程化的第二种方式:action-types。对派发行为标识的统一管理-即宏管理。
- 原因:
- dispatch派发的时候,要求传递的action.type必须和reducer中判断比较的action.type 高度一致(区分大小写);所以如有提示机制,这样不容易出错!
- 这个提示是指的编辑器名称提示,编写时直接按提示来,就不会出现丢一个字母或多一个字母的问题,或者是写错的问题。
- 每一次dispatch派发,会把所有的reducer都执行,也就是传递的action.type会进入所有的reducer中进行判断匹配;所以在多人协作开发的时候,很容易两个开发者定义了相同名字的行为标识,这样不论其中谁进行派发,都会对另外的模块产生影响!
- …
- dispatch派发的时候,要求传递的action.type必须和reducer中判断比较的action.type 高度一致(区分大小写);所以如有提示机制,这样不容易出错!
- 具体步骤:
-
新建一个文件,存放所有的行为标识。
- 各个模块下,需要派发的判断的行为标识,都在这个文件中进行统一的管理。
- 可以保证,标识是不会冲突的。因为变量名和变量值保持一致,如果冲突了,那么变量名冲突就会报错了。
- 代码:
-
/src/store/action-types.js
// 各个模块下,需要派发的判断的行为标识,都在这个文件中进行统一的管理。 // 可以保证,标识是不会冲突的。因为变量名和变量值保持一致,如果冲突了,那么变量名冲突就会报错了。 export const VOTE_SUP = "VOTE_SUP"; export const VOTE_OPP = "VOTE_OPP";export const TASK_QUERY_LIST = "TASK_QUERY_LIST";
-
- 各个模块下,需要派发的判断的行为标识,都在这个文件中进行统一的管理。
-
独立的reducer根据统一标识符进行逻辑处理。
- 代码:
-
/src/store/reducers/voteReducer.js
import * as AT from "../action-types"; //导入所有统一管理的行为标识。 // import * as AT from '../action-types'let initial = {title: "React不是很难的!",supNum: 10,oppNum: 5, }; export default function voteReducer(state = initial, action) {let { type } = action;// 基于统一管理的标识进行判断,而且写代码时有代码提示。switch (type) {case AT.VOTE_SUP:state.supNum++;break;case AT.VOTE_OPP:state.oppNum++;break;default:}return state; }
-
/src/store/reducers/taskReducer.js
import * as AT from "../action-types"; let initial = {title: "TASK OA 任务管理系统",list: [], }; export default function taskReducer(state = initial, action) {let { type } = action;switch (type) {case AT.TASK_QUERY_LIST:// ...break;default:}return state; }
-
- 代码:
-
业务组件调用时,dispatch中用到的行为对象中type类似使用统一标识符。
- 代码:
-
/src/views/VoteFooter.jsx
import React, { useContext } from "react"; import { Button } from "antd"; import Theme from "@/Theme"; import * as AT from '@/store/action-types'//组件中派发的时候,也是用统一管理的标识进行派发。const VoteFooter = function VoteFooter() {const { store } = useContext(Theme);return (<div className="footer-box"><Buttontype="primary"onClick={() => {store.dispatch({type: AT.VOTE_SUP,});}}>支持</Button><Buttontype="primary"dangeronClick={() => {store.dispatch({type: AT.VOTE_OPP,});}}>反对</Button></div>); };export default VoteFooter;
-
- 代码:
-
- 原因:
-
redux工程化的第三种方式:actionCreators。对派发的action行为对象进行统一管理-分模块管理。
-
单纯这样看,本次工程化一点用都没有,而且还让代码写起来更麻烦。
-
实际步骤:
-
新建多个
独立action文件
,内部返回一个独立action对象
。-
独立action对象
是内部存放多个返回具体reducer所需行为对象的函数
。 -
代码:
-
/src/store/actions/voteAction.js
import * as AT from "../action-types"; //一个对象中包含多个方法,每个方法执行返回的就是dispatch时需要传递的action对象。 const voteAction = {support() {return {type: AT.VOTE_SUP,};},oppose() {return {type: AT.VOTE_OPP,};}, }; export default voteAction;
-
-
代码:
-
/src/store/actions/taskAction.js
import * as AT from "../action-types"; //一个对象中包含多个方法,每个方法执行返回的就是dispatch时需要传递的action对象。 const taskAction = {queryTaskList() {return {type: AT.TASK_QUERY_LIST,};}, }; export default taskAction;
-
-
-
将多个
独立action对象
组合成一个总action对象
,并导出。- 代码:
-
/src/store/actions/index.js
/* // 把各个模块的action管理对象,合并为一个总的对象 action = {vote: {support() {return {type: AT.VOTE_SUP,};},oppose() {return {type: AT.VOTE_OPP,};},},task: {//...}, }; */import voteAction from "./voteAction"; import taskAction from "./taskAction"; const action = {vote: voteAction,task: taskAction, }; export default action;
-
- 代码:
-
业务组件调用时,dispatch中用到的行为对象是由
总action对象
中包含的某个独立action文件
的某个返回具体reducer所需行为对象的函数
被调用时所生成的。- 代码:
-
/src/views/VoteFooter.jsx
import React, { useContext } from "react"; import { Button } from "antd"; import Theme from "@/Theme"; import action from "@/store/actions";const VoteFooter = function VoteFooter() {const { store } = useContext(Theme);return (<div className="footer-box"><Buttontype="primary"onClick={() => {store.dispatch(action.vote.support());}}>支持</Button><Buttontype="primary"dangeronClick={() => {store.dispatch(action.vote.oppose());}}>反对</Button></div>); };export default VoteFooter;
-
- 代码:
-
-
通过上方的代码应用和分析,发现本次工程化的方式,一点用都没有,而且写起来更复杂了。
- 本次处理,它的优势和作用:
- 主要是在使用react-redux的时候,才会看到它的好处。
- 这样处理也是为了方便,后期基于
redux中间件
实现异步派发,效果上类似于vuex中的actions异步派发管理。
- 本次处理,它的优势和作用:
-
redux使用的总步骤
react-redux
- react-redux的意义与作用:简化redux在项目中的应用。
react-redux使用步骤
- react-redux 提供了一个 Provider 组件,这个组件可以帮助我们把 store 放在上下文信息中!
- react-redux 提供了一个 connect 高阶组件:
-
作用:
- 可以自动获取上下文信息中的store,并且获取了所有的状态…
- 默认向事件池中加入让组件更新的办法「无需我们自己增加」
- …
-
基于connect函数操作:
-
优化前:
-
代码:
import VoteStyle from "./VoteStyle"; import VoteMain from "./VoteMain"; import VoteFooter from "./VoteFooter"; import React, { useContext } from "react"; import Theme from "@/Theme"; import useForceUpdate from "@/useForceUpdate"; // import { connect } from "react-redux";const Vote = function Vote(props) {const { store } = useContext(Theme);let { supNum, oppNum, title } = store.getState().vote;useForceUpdate(store);// let { title, supNum, oppNum } = props;return (<VoteStyle><h2 className="title">{title}<span>{supNum + oppNum}</span></h2><VoteMain /><VoteFooter /></VoteStyle>); }; export default Vote;
-
-
优化后:
-
代码:
import VoteStyle from "./VoteStyle"; import VoteMain from "./VoteMain"; import VoteFooter from "./VoteFooter"; import { connect } from "react-redux"; const Vote = function Vote(props) {let { title, supNum, oppNum } = props;return (<VoteStyle><h2 className="title">{title}<span>{supNum + oppNum}</span></h2><VoteMain /><VoteFooter /></VoteStyle>); }; export default connect((state) => {return state.vote; })(Vote);
// import React, { useContext } from "react"; // import Theme from "@/Theme"; // import useForceUpdate from "@/useForceUpdate"; import { connect } from "react-redux"; const VoteMain = function VoteMain(props) {// const { store } = useContext(Theme);// let { supNum, oppNum } = store.getState().vote;// useForceUpdate(store);let { supNum, oppNum } = props;return (<div className="main-box"><p>支持人数:{supNum} 人</p><p>反对人数:{oppNum} 人</p></div>); }; // export default VoteMain; export default connect((state) => state.vote)(VoteMain);
-
-
-
connect:react-redux提供的高阶函数。
-
获取基于Provider放在上下文中的store对象。
-
把让组件更新的办法自动加入到事件池中。
-
语法:
export default connect([mapStateToProps],[mapDispatchToProps], )(需要渲染的组件)
- [mapStateToProps]:内部获取总的公共状态,把需要的公共状态,最后基于属性传递给需要渲染的组件。
- 类型:
-
函数
。connect(state=>{//state:总的状态。return {//返回对象中的信息,最后会作为属性传递给组件。//...}})
-
null
/undefined
。
-
- [mapDispatchToProps]:把我们需要派发的方法,基于属性传递给组件。
- 类型:
-
函数。
(dispatch)=>{//dispatch:store.dispatch 用来派发任务的方法。return {//返回对象中的信息,会基于属性传递给组件。support(){return dispatch(action.vote.support())//{ type: AT.VOTE_SUP, }}} }
-
actionCreator对象:
-
actionCreator对象:一个对象中包含好多方法,每个方法执行,老师返回需要派发的action对象。
- action对象,即dispatch()入参的对象,至少有一个type属性的纯对象。
-
connect内部,自动基于bindActionCreators方法,把actionCreator对象,变成mapDispatchToProps这样的函数模式。
// action.vote 「actionCreator对象:对象中包含好多方法,每个方法执行,都是返回需要派发的action对象」 //假设传入的是action.vote,而action.vote的样式为下方那样: {support() {return {type: AT.VOTE_SUP}},oppose() {return {type: AT.VOTE_OPP}} } // 基于 bindActionCreators(action.vote,dispatch) 处理后: {support(){return dispatch(action.vote.support())},oppose(){return dispatch(action.vote.oppose())} }
-
-
null/undefined。
-
- 语法:
-
函数:
import React from "react"; import { Button } from "antd"; import { connect } from "react-redux"; import action from "@/store/actions"; const VoteFooter = function VoteFooter(props) {let { support, oppose } = props;return (<div className="footer-box"><Buttontype="primary"onClick={() => {support();}}>支持</Button><Buttontype="primary"dangeronClick={() => {oppose();}}>反对</Button></div>); }; export default connect(null, (dispatch) => {return {support() {console.log("support()");return dispatch(action.vote.support());},oppose() {console.log("oppose()");return dispatch(action.vote.oppose());},}; })(VoteFooter);
- 这样处理很麻烦,我们一般都是把它设置为
actionCreators对象格式
。
- 这样处理很麻烦,我们一般都是把它设置为
-
actionCreator对象:
import React from "react" import { Button } from 'antd' import { connect } from '@/myreactredux' import action from "@/store/actions"const VoteFooter = function VoteFooter(props) {let { support, oppose } = propsreturn <div className="footer-box"><Button type="primary"onClick={support}>支持</Button><Button type="primary" dangeronClick={oppose}>反对</Button></div> } export default connect(null, action.vote)(VoteFooter)
-
- 类型:
- 类型:
- [mapStateToProps]:内部获取总的公共状态,把需要的公共状态,最后基于属性传递给需要渲染的组件。
-
-
除了基于 connect 函数「适用于所有类型的组件」可以做这个事情,react-redux还提供了一些Hook函数「只适用于函数组件」,也可以实现类似的效果!
-
常用的:useSelector, useStore, useDispatch。
-
useSelector: 是react-redux提供的自定义Hook函数,只能在函数组件中运用。
-
获取基于Provider放在上下文中的store对象。
-
把让组件更新的办法自动加入到事件池中。
-
useSelector语法
let xxx = useSelector((state) => {//必须传递一个函数。//state是公共容器中的总状态。return {//返回的对象中包含我们需要的状态信息,整体赋值给外面的xxx。a:state.vote.supNum,b:state.task.title,//....} }) //xxx.a --> state.vote.supNum //xxx.b --> state.task.title
import { useSelector,useStore } from "react-redux";// const VoteMain = function VoteMain() {let xxx = useSelector((state) => {console.log('state',state);return {a:state.vote.supNum,b:state.task.list,}})console.log(xxx);console.log('useStore()',useStore());let { supNum, oppNum } = useSelector((state) => state.vote);return (<div className="main-box"><p>支持人数:{supNum} 人</p><p>反对人数:{oppNum} 人</p></div>); }; export default VoteMain;
-
-
useStore: 把上下文中的store对象获取到。
import { useSelector,useStore } from "react-redux";// const VoteMain = function VoteMain() {console.log('useStore()',useStore());let { supNum, oppNum } = useSelector((state) => state.vote);return (<div className="main-box"><p>支持人数:{supNum} 人</p><p>反对人数:{oppNum} 人</p></div>); }; export default VoteMain;
-
useDispatch: 得到store对象的dispatch()方法。
import React from "react"; import { Button } from "antd"; import { useDispatch } from "react-redux"; import action from "@/store/actions";const VoteFooter = function VoteFooter() {const dispatch = useDispatch();return (<div className="footer-box"><Buttontype="primary"onClick={() => {dispatch(action.vote.support());}}>支持</Button><Buttontype="primary"dangeronClick={() => {dispatch(action.vote.oppose());}}>反对</Button></div>); }; export default VoteFooter;
-
-
bindActionCreators
-
/src/views/VoteFooter.jsx
function bindActionCreator(actionCreator, dispatch) {return function () {return dispatch(actionCreator.apply(this, arguments))} } export default function bindActionCreators(actionCreators, dispatch) {if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, dispatch)}if (typeof actionCreators !== 'object' || actionCreators === null) {throw new Error(`bindActionCreators expected an object or a function, but instead received: '${kindOf(actionCreators)}'. ` +`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`)}const boundActionCreators = {}for (const key in actionCreators) {const actionCreator = actionCreators[key]if (typeof actionCreator === 'function') {boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)}}return boundActionCreators }
react-redux细节
- react-redux内部有一个细节:
-
我们讲的,其默认向事件池中加入让组件更新的办法「无需我们自己增加」;方法确实加了,但是加的方法有特殊处理!!
- 当加入的方法被执行的时候(也就是状态更改的时候),它会判断新老状态是否一致(直接比较内存地址),如果是相同的内存地址,则禁止视图更新!
-
而我们在讲 combineReducers 源码的时候发现,如果我们的reducer这样写:
export default function taskReducer(state = initial, action) {let { type } = actionswitch (type) {//...default:}return state }
- 这样redux容器中的状态,一直使用的是相同的堆内存地址;这样在经过react-redux处理的时候,会认为其没更改,视图也就不会更新了!
-
所以我们在写reducer的时候,进来的第一件事情,就是把 传递的原有的 state 进行拷贝!
export default function taskReducer(state = initial, action) {state = { ...state };let { type } = actionswitch (type) {//...default:}return state }
-
- 这样redux容器中的状态,一直使用的是相同的堆内存地址;这样在经过react-redux处理的时候,会认为其没更改,视图也就不会更新了!
-
react-redux源码
-
react-redux源码可在node_modules里查看:
/node_modules/react-redux/dist/react-redux.js
。 -
/src/myreactredux.jsx
import { createContext, useContext, useEffect, useState } from "react"; import { bindActionCreators } from "redux"; import PT from "prop-types"; import _ from "@/assets/utils";/* 创建上下文对象 */ const ThemeContext = createContext();/* Provider:把基于属性传递进来的store对象,放在上下文中 */ export const Provider = function Provider(props) {let { store, children } = props;return (<ThemeContext.Providervalue={{store,}}>{children}</ThemeContext.Provider>); }; Provider.propTypes = {store: PT.object.isRequired, };/* 提供三个自定义Hook函数 */ export const useStore = function useStore() {let { store } = useContext(ThemeContext);return store; }; export const useDispatch = function useDispatch() {const store = useStore();return store.dispatch; }; export const useSelector = function useSelector(callback) {if (typeof callback !== "function")throw new TypeError(`useSelector 中的 callback 必须是一个函数`);// 获取总状态const store = useStore(),state = store.getState();// 向redux事件池中,注入让组件更新的办法let [, setRandom] = useState(+new Date());useEffect(() => {let unsubscribe = store.subscribe(() => {// 其内部在让组件更新之前做了优化:对比新老状态(浅比较),如果不一样,才让组件更新setRandom(+new Date());});return () => unsubscribe();}, []);// 把传递的 callback 函数执行,传递总状态,接收返回的对象(包含需要用的状态)let result = callback(state);if (!_.isPlainObject(result))throw new TypeError(`callback 执行,必须返回的一个对象`);return result; };/* 核心方法:connect */ export const connect = function connect(mapStateToProps, mapDispatchToProps) {// 处理参数为空的情况if (mapStateToProps == null) {mapStateToProps = () => {return {};};}if (mapDispatchToProps == null) {mapDispatchToProps = () => {return {};};}return function (Component) {// Component:最终需要渲染的组件// 还需要返回一个供外面调用的组件「HOC:Higher-Order Components 」return function HOC(props) {// 传递给HOC组件的属性,其实也是要传递给Vote的let attrs = {...props,};// 处理 mapStateToProps「直接用 useSelector 处理即可,其内部把事件池的操作都处理了」let state = useSelector(mapStateToProps);Object.assign(attrs, state);// 处理 mapDispatchToPropslet temp = {};let dispatch = useDispatch();if (typeof mapDispatchToProps === "function") {// 是函数的情况:把函数执行,返回的信息就是需要基于属性传递给组件的temp = mapDispatchToProps(dispatch);} else if (_.isPlainObject(mapDispatchToProps)) {// 是对象的情况:说明其实 actionCreators 对象,我们需要基于 bindActionCreators 处理temp = bindActionCreators(mapDispatchToProps, dispatch);}if (!_.isPlainObject(temp))throw new TypeError(`mapDispatchToProps 处理的结果需要是一个对象`);Object.assign(attrs, temp);// 但是最终要渲染的还是之前传递的组件(例如:Vote),并且把 mapStateToProps&mapDispatchToProps 等处理后的结果,通过属性传递给这个组件return <Component {...attrs} />;};}; };
高阶组件
- HOC: Higher-Order Components高阶组件。
-
通过高级函数-即闭包函数返回的组件。
// @HOC class Index extends React.Component{say(){const { name } = this.propsconsole.log(name)}render(){return <div> hello,world <button onClick={ this.say.bind(this) } >点击</button> </div>} }function HOC(Component) {return class wrapComponent extends React.Component{constructor(){super()this.state={name:'alien'}}render=()=><Component { ...this.props } { ...this.state } />} } HOC(Index)//会返回一个wrapComponent组件,但wrapComponent组件实际渲染的是传入的Index组件。不过wrapComponent组件内部中的Index组件有了wrapComponent组件实例上的state实例状态及父组件调用wrapComponent组件时传给wrapComponent组件的props。
- 上方HOC就是高阶组件,它在传入一个组件类之后,就会返回一个新外部组件类。不过新外部组件类并不实际渲染其它内容,渲染的只是传入HOC函数中的组件类。但实际渲染过程中,传入的组件类可以通过作用域链或原型链来访问原本身上所没有的变量或状态。
-
进阶参考
- 「react进阶」一文吃透React高阶组件(HOC)