组件公共状态管理react-redux
知乎日报项目中,公共状态使用了 redux 和 react-redux,记录学习的知识
redux 工程化其实就是按模块划分,在开发中能更好的理解和维护,因此该项目将状态管理划分为如下的模块:
- store 用作存放状态管理的文件夹
- action 是 store 中数据的来源,actions 文件夹用来管理派发行为对象的,index.js是将其他的 action 全部都合并到一个 action 中,并且暴露出去,该文件夹下其他的 js 文件都是针对不同功能的行为对象
- reducer 文件夹是 reducer 的模块化管理,通过派发的不同行为对象标识(type)来执行不同的操作(状态处理函数),index.js是将 reducer 全都合并到一个 reducer 中,并且暴露出去
- action-types.js 文件存放不同的行为对象标识符(type)
- index.js 是用来创建并暴露 store
下面为每个文件的具体代码
// store/actions/base.js
import * as TYPES from '../action-types';
import api from '../../api';
const baseAction = {
// 异步获取登陆者信息,完成派发
async queryUserInfoAsync() {
let info = null;
try {
let { data, code } = await api.queryUserInfo();
if (+code === 0) {
info = data;
}
} catch (error) { }
return {
type: TYPES.BASE_INFO,
info
}
},
// 清除存储的登录者信息
clearUserInfo() {
return {
type: TYPES.BASE_INFO,
info: null
}
}
};
// 暴露
export default baseAction;
⬆ 我们也可以看出其实action就是一个普通对象,只不过必须包含type标识符,同时需要有数据(可以从后端获取也可以接收传递的数据)传递给 reducer 进行处理
// store/actions/index.js
import baseAction from './base';
import storeAction from './store';
const actions = {
base: baseAction,
store: storeAction
};
export default actions;
⬆ 该文件用于整合所有的 action,进行统一暴露处理
// store/reducer/base.js
import * as TYPES from '../action-types';
import _ from '../../assets/utils';
let inital = {
info: null
}
// 给state设置初始值
export default function baseReducer(state=inital,action) {
// 因为 reducer 不能返回原来的对象,所以需要将对象进行克隆,因此在 case 中最好不要出现 push 等数组操作,因为并没有修改原来的对象或数组的地址,可以使用浅克隆{...state},我这里使用了深克隆
state=_.clone(state);
// 使用 switch 语句进行逐个匹配
switch(action.type){
// 更新登陆者信息,在这里使用标识符进行匹配,减少错误,方便差错和维护
case TYPES.BASE_INFO:
state.info=action.info;
break;
// 也可以写成 return state.info=action.info;
default:
}
return state;
};
⬆ 对存储的状态进行处理
import { combineReducers } from 'redux';
import baseReducer from './base';
import storeReducer from './store';
// 使用 combineReducers 将状态处理函数进行整合
const reducer = combineReducers({
base:baseReducer,
store:storeReducer
});
export default reducer;
⬆ 整合状态处理函数
注意:所有reducer的合并,并不是代码的合并,而是创建一个总的reducer出来,每一次的派发,都是让总的reducer执行,在这里是会把每个reducer都完整执行一遍,即使中间已经发现匹配的reudcer,也会继续把其他模块中的reducer执行(不太好)
// store/action-types.js
export const BASE_INFO = "BASE_INFO";
export const STORE_LIST = "STORE_LIST";
export const STORE_REMOVE = "STORE_REMOVE";
⬆ 定义对象标识符
import { createStore, applyMiddleware } from 'redux';
import reduxLogger from 'redux-logger';
import reduxThunk from 'redux-thunk';
import reduxPromise from 'redux-promise';
import reducer from './reducer';
// 根据不同的环境使用不同的中间件,Thunk和Promise都是用于处理异步的状态
let middleware = [reduxThunk, reduxPromise],
// 存放了当前的环境变量
env = process.env.NODE_ENV;
// 如果当前环境为开发环境,就会有 reduxLogger,即每次状态改变都会打印路由状态信息
if (env === 'development') {
middleware.push(reduxLogger);
}
// 创建store并使用中间件
const store = createStore(
reducer,
applyMiddleware(...middleware)
);
export default store;
⬆ 查看当前环境变量 ⬇
⬇ reduxLogger 的提示
为了能在组件中都能实现数据共享,需要在
import { Provider } from 'react-redux';
import store from './store';
// ······其他配置项
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
// ConfigProvider 是antd-mobile的配置项,用来配置语言选项
<ConfigProvider local={zhCN}>
<Provider store={store}>
<App />
</Provider>
</ConfigProvider>
);
在需要使用store的组件中需要使用connect关键字,就可以在组建的props中获取需要的方法 ⬇
import { connect } from "react-redux";
import actions from "../store/actions";
const Detail = function Detail(props) {
console.log(props)
};
export default connect(
state => {
return {
base: state.base,
store: state.store
}
},
{ ...actions.base, ...actions.store }
)(Detail);
connect 的第一个括号内也可以直接写 actionCreator 对象(例如在 store/action/base.js 中暴露的 baseAction 对象),内部会基于 bindActionDispatch 方法,把 actionCreator 对象转变为标准的 mapDispatchToProps这种格式 ⬇
// actions.base 是 actionCreator 对象
export default connect(
null,
actions.base
)(Detail);
// 会转变为标准版格式
export default connect(
null,
dispatch => {
return {
queryUserInfoAsync() {
dispatch(actions.base.queryUserInfoAsync());
},
clearUserInfo() {
dispatch(actions.base.clearUserInfo());
}
}
}
)(Detail);
props如下图 ⬇ :
// 将所需的方法解构出来后就可以直接使用了
let {
base: { info: userInfo }, queryUserInfoAsync,
location,
store: { list: storeList }, queryStoreListAsync, removeStoreListById
} = props;
useEffect(() => {
(async () => {
// 第一次渲染完,如果userInfo不存在,需要派发任务同步登陆者信息
if (!userInfo) {
let { info } = await queryUserInfoAsync();
userInfo = info;
}
// 已经登录 && 没有收藏列表
if (userInfo && !storeList) {
queryStoreListAsync();
}
})();
}, []);
redux 的缺陷
- 我们基于 getState 后取得公共状态,是直接和 redux 中的公共状态,公用相同的地址,这样到时我们可以直接使用 getState 的数据来直接修改公共状态信息的,这与 redux 的派发流程,在 reducer 中统一修改的想法不符
- 优化思路:总体思路就是返回值和 redux 中的公共状态不应该是相同的地址,再返回的时候,对状态做深拷贝处理
- 我们把让组件更新的方法,放在事件池中,当公共状态改变时,会通知事件池中所有的方法执行,这样的话如论哪个状态被修改,事件池中所有的方法都要执行,相关的组件也都会更新
- 优化思路:给事件池中的方法设置依赖,在每次执行 reducer 之前,把之前的状态存储一份,修改后的状态也存储一份,通知事件池中方法执行的时候,判断某个方法是否会执行,在存储的两个备份中此方法以来的状态是否改变
- 但是在 SPA 组件中,没必要进行优化,因为 react-router 会让很多组件释放掉,只展示当前页面,这样即使组件更新的方法执行,但因为组件都释放掉了,也不会产生太大的影响,我们可以在组件释放掉时,把对应更新的方法从事件池中移除
- 所有reducer的合并,并不是代码的合并,而是创建一个总的reducer出来,每一次的派发,都是让总的reducer执行,在这里是会把每个reducer都完整执行一遍,即使中间已经发现匹配的reudcer,也会继续把其他模块中的reducer执行
- 优化思路:在某个模块的 reducer 中,如果派发的行为标识有匹配了,就停止执行后面的 reducer