🥳一招破解React组件重复请求——React业务实践

1,059 阅读3分钟

你好,我是本文作者南一。如果有发现错误或者可完善的地方,恳请斧正,万分感谢!!

背景

日常做业务过程,经常需要将组件和接口封装在一起,确保每个组件独立维护自己的状态。但是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 />);

image.png

这种写法会有隐患:

  1. 一个页面如果同时挂载多个CustomSelect组件,获取options的请求会被重复发送
  2. 组件被多次挂载卸载,也会重复发送请求,例如CustomSelect组件被放在弹窗表单中

解决方案

直接缓存请求,不考虑组件挂载和更新的情况。

实现缓存函数需要考虑以下问题:

  1. 缓存键的唯一性保证
  2. 并发请求的合并处理
  3. 过期机制与清理策略

数据结构设计

两层Map,第一层缓存相同请求的所有响应值。第二层缓存同一个请求的不同参数的响应值

  1. 缓存键的唯一性:以参数为key,需要对参数做唯一性处理,本文是采用将参数对象去空值、排序、转换为字符串的方式处理。
  2. 过期机制:采用定时器处理,在缓存失效时进行清除,注意记得清除定时器,防止内存泄漏。
  3. 并发请求:慢触发的请求会使用第一次缓存的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);

结果

同一个请求只会发送一次

image.png

总结

该方式能有效减少重复请求数量,降低心智负担。当然还有很多优化的地方,例如可以对函数做防抖处理。