你好,我是本文作者南一。如果有发现错误或者可完善的地方,恳请斧正,万分感谢!!
背景
日常做业务过程,经常需要将组件和接口封装在一起,确保每个组件独立维护自己的状态。但是React的更新机制会导致出现很多预期之外的请求,造成资源浪费。
案例
import { useState, useEffect } from "react";
import { createRoot } from "react-dom";
import { Select } from "antd";
/**
* 模拟网络请求获取数据选择框数据
* params 是传入接口的参数 这里只是简单模拟
*/
const requestOptions = (params) => {
return new Promise((resolve, reject) => {
const options = [
{ name: 'aaaa', id: 1, status: 1 },
{ name: 'bbbb', id: 2, status: 0 },
{ name: 'cccc', id: 3, status: 1 },
{ name: 'dddd', id: 4, status: 0 },
];
if(typeof params?.status === "number") {
resolve(options.filter(item => item.status === params.status));
} else {
resolve(options);
}
})
}
const CustomSelect = ({ status, onRun, ...props }) => {
const [options, setOptions] = useState([]);
const request = () => {
onRun();
requestOptions({ status }).then(res => {
setOptions(res.map(item => ({
value: item.id,
label: item.name
})));
});
}
useEffect(() => {
request();
// cacheRequest(request)();
}, [status]);
return (
<Select
options={options}
style={{ width: 200 }}
{...props}
/>
)
}
const Wrapper = () => {
const [count, setCount] = useState(0);
const plus1 = () => {
setCount((pre) => pre + 1);
}
return <div className="wrapper">
<h3>CustomSelect 被执行了{count}次</h3>
<CustomSelect status={1} onRun={plus1}/>
<CustomSelect status={0} onRun={plus1}/>
<CustomSelect status={1} onRun={plus1}/>
<CustomSelect status={0} onRun={plus1}/>
</div>
}
const app = document.getElementById('app');
const root = createRoot(app);
root.render(<Wrapper />);
这种写法会有隐患:
- 一个页面如果同时挂载多个
CustomSelect组件,获取options的请求会被重复发送 - 组件被多次挂载卸载,也会重复发送请求,例如
CustomSelect组件被放在弹窗表单中
解决方案
直接缓存请求,不考虑组件挂载和更新的情况。
实现缓存函数需要考虑以下问题:
- 缓存键的唯一性保证
- 并发请求的合并处理
- 过期机制与清理策略
数据结构设计
两层Map,第一层缓存相同请求的所有响应值。第二层缓存同一个请求的不同参数的响应值
- 缓存键的唯一性:以参数为key,需要对参数做唯一性处理,本文是采用将参数对象去空值、排序、转换为字符串的方式处理。
- 过期机制:采用定时器处理,在缓存失效时进行清除,注意记得清除定时器,防止内存泄漏。
- 并发请求:慢触发的请求会使用第一次缓存的Promise。
const requestCache: Map<string, Map<string, Promise<any>>> = new Map();
const defaultCacheTime = 300000; // 5分钟
export const cacheRequest = <TData, TParams>(service: (params?: TParams) => Promise<TData>, cacheTime: number = defaultCacheTime) => async (params?: TParams): Promise<TData> => {
if(!requestCache.has(service.name)) {
requestCache.set(service.name, new Map());
}
const cache = requestCache.get(service.name);
const cacheKey = getCacheKey(sortObjectKeys(params));
if(!cache.has(cacheKey)) {
cache.set(cacheKey, service(params));
// 缓存5分钟
const timeoutId = setTimeout(() => {
cache.delete(cacheKey);
clearTimeout(timeoutId);
}, cacheTime);
}
return await cache.get(cacheKey);
}
function sortObjectKeys(obj: Record<string, any>) {
if (!obj) return {};
const keys = Object.keys(removeEmptyValues(obj));
const sortedKeys = keys.sort();
const sortedObj = sortedKeys.reduce((accumulator, key) => {
accumulator[key] = obj[key];
return accumulator;
}, {});
return sortedObj;
}
function getCacheKey(params?: Record<string, any>): string {
if (!params) return "";
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
searchParams.append(key, JSON.stringify(value));
}
return searchParams.toString();
}
/**
* 去除对象中的 null undefined 空对象 空数组 空字符串
* @param obj 需要过滤的对象
* @returns 返回新对象
*/
const removeEmptyValues = (obj: Record<string, any>) => {
return Object.keys(obj).reduce((acc, key) => {
if (
obj[key] === null
|| obj[key] === undefined
|| (typeof obj[key] === 'object' && Object.keys(obj[key]).length === 0)
|| (Array.isArray(obj[key]) && obj[key].length === 0)
|| (typeof obj[key] === 'string' && obj[key].trim() === '')
) {
return acc;
}
acc[key] = obj[key];
return acc;
}, {});
}
使用方式,用cacheRequest函数,将请求函数包裹一层
const request = cacheRequest(requestOptions);
结果
同一个请求只会发送一次
总结
该方式能有效减少重复请求数量,降低心智负担。当然还有很多优化的地方,例如可以对函数做防抖处理。