throttle与debounce
- 一般直接使用现有的lodash库,如果只是单纯希望实现throttle和debounce的简易功能,不需要引入其他的一些工具,为了打包结果更小可以采用自己写函数的方式
throttle: 同时间段内多次触发只有第一次生效
常见应用场景:
- 防止button被频繁点击,触发多个弹层功能
- 滚动加载更多场景
debounce:同时间段内多次触发只有最后一次生效
常见应用场景:
- 输入框,一般希望用户输入完成之后再触发搜索功能
- 窗口拖拽场景,resize之后只在最后实现实际函数
除此之外还可以在某些场景用于请求控制,典型场景:
- 同时监听tab和下拉框选中信息以及搜索框的信息,用于更新表格信息。如果搜索有值的情况希望在切换Tab的情况清空对应的搜索值,返回新tab页新下拉框选中的信息数据,这个时候其实会发送两次请求:
- 切换tab,获取新的下拉框信息,并由此获取列表数据,并清空对应搜索项
- 监听搜索项变化,获取新的列表数据
实际只需要获得最后一次的请求数据,但是因为需求问题,有时候不可避免要多个watch监听不同的条件,导致同时触发多个请求,请求因为时延问题,不一定会按照发送顺序接收,最终导致有用的信息被后续到的请求覆盖 ,这个时候因为请求发送的时机是可控的(接收时机受后端和网络时延往往不可控),可以使用throttle和debounce进行请求控制
请求竞态AbortController
结合请求拦截器和 AbortController
处理请求竞态,可以实现 全局统一管理重复请求 的效果:当相同的请求(如同一接口、同一参数)重复发送时,自动取消前序未完成的请求,只保留最后一次请求。这种方式无需在每个请求处单独处理,更适合大型项目。
js
import axios from 'axios';
// 1. 创建Axios实例
const instance = axios.create({
baseURL: '/api',
timeout: 5000
});
// 2. 存储当前活跃的请求控制器:key为请求唯一标识,value为AbortController实例
const requestControllers = new Map();
/**
* 生成请求唯一标识(确保相同请求生成相同key)
* @param {Object} config - Axios请求配置
* @returns {string} 唯一标识字符串
*/
const generateRequestKey = (config) => {
const { url, method, params, data } = config;
// 组合URL、方法、参数生成key(忽略无关配置,确保相同请求生成相同key)
return [
url,
method?.toUpperCase(),
JSON.stringify(params || {}),
JSON.stringify(data || {})
].join('&');
};
// 3. 请求拦截器:处理重复请求,取消前序请求
instance.interceptors.request.use(
(config) => {
// 生成当前请求的唯一标识
const requestKey = generateRequestKey(config);
// 若存在前序相同请求的控制器,取消它
if (requestControllers.has(requestKey)) {
const prevController = requestControllers.get(requestKey);
prevController.abort(); // 取消前序请求
requestControllers.delete(requestKey); // 移除旧控制器
console.log(`已取消重复请求: ${requestKey}`);
}
// 创建新的AbortController,关联当前请求
const controller = new AbortController();
config.signal = controller.signal; // 将signal加入请求配置
// 存储新控制器
requestControllers.set(requestKey, controller);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 4. 响应拦截器:清理控制器(无论成功/失败)
instance.interceptors.response.use(
(response) => {
const requestKey = generateRequestKey(response.config);
requestControllers.delete(requestKey); // 请求完成,移除控制器
return response;
},
(error) => {
// 处理取消请求的错误(不视为业务错误)
if (error.name === 'CanceledError') {
console.log('请求已被取消(重复请求)');
return Promise.resolve(null); // 或根据业务需求处理
}
// 其他错误:清理控制器并抛出
if (error.config) {
const requestKey = generateRequestKey(error.config);
requestControllers.delete(requestKey);
}
return Promise.reject(error);
}
);
如果某些接口需要允许并发(不取消前序请求),可通过请求配置中的 allowConcurrent
字段例外处理
arduino
// 修改请求拦截器逻辑,增加例外判断
instance.interceptors.request.use((config) => {
// 若配置 allowConcurrent: true,则不取消前序请求
if (config.allowConcurrent) {
return config;
}
// 否则执行原逻辑(取消重复请求)
const requestKey = generateRequestKey(config);
// ... 剩余逻辑不变
});
// 使用时允许并发
fetchData(1, { allowConcurrent: true }); // 此请求不会被后续相同请求取消
Q: 按照上述代码,响应拦截器的时候没办法删除map内的key,显示没有这个key
A: 出现这种情况通常是因为 请求拦截器和响应拦截器中生成的 requestKey
不一致 导致的(例如请求配置在发送过程中被修改)。解决方法是在请求拦截器中生成 requestKey
后直接存储到 config
中,在响应拦截器中直接使用该 key
而不是重新生成
ini
instance.interceptors.request.use(
(config) => {
const requestKey = generateRequestKey(config);
// 关键:将key存储到config中,确保响应时能获取到相同的key
config.__requestKey = requestKey;
if (requestControllers.has(requestKey)) {
const prevController = requestControllers.get(requestKey);
prevController.abort();
requestControllers.delete(requestKey);
}
const controller = new AbortController();
config.signal = controller.signal;
requestControllers.set(requestKey, controller);
return config;
},
(error) => Promise.reject(error)
);
instance.interceptors.response.use(
(response) => {
// 关键:直接使用请求时存储的key
const requestKey = response.config.__requestKey;
if (requestKey && requestControllers.has(requestKey)) {
requestControllers.delete(requestKey);
}
return response;
},
(error) => {
if (error.config && error.config.__requestKey) {
const requestKey = error.config.__requestKey;
if (requestControllers.has(requestKey)) {
requestControllers.delete(requestKey);
}
}
if (error.name === 'CanceledError') {
return Promise.resolve(null);
}
return Promise.reject(error);
}
);
Tailwindcss原子化CSS
css类原子化,使得公用的css代码不用再次生成,可以导致最终生产环境打包的css代码体积减小
useMemo && useCallback && react.memo
- 静态组件(指只收到外部输入props影响变更的组件,不受ref/useState等影响),可以使用react.memo包裹,去提高渲染的复用率
- 静态函数如果需要缓存结果或者函数本身则可以使用useMemo和useCallback处理
- 一般常见于react,vue一般不需要使用以上方式
- 因为react的渲染机制影响,一旦父组件更新,内部全部子组件也会重渲染 ,这导致了为了优化渲染性能,会使用以上方式缓存不要变化的子组件;但是vue的渲染机制追踪的粒度更细,能够索引到对应的响应式数据上,进行局部变化,父组件某个变量更新并不会导致整个子组件重渲染
- vue在某些特殊场景下:大量纯数据渲染列表组件(渲染耗时长,变化不频繁),可以使用v-memo去实现包裹
懒加载
- 路由懒加载,基本是所有项目都会用的手段
- 其他包括图片懒加载等,都可以提高对应的性能
优化项目代码结构
- 比如监听watch多个变量,都需要触发同一个函数,并且多个变量之间会有触发重合的现象,就可以使用一个watch监听触发该函数,这样可以减少函数的多次不必要请求
js
const handleChange = debounce(()=>{/.../},100)
watch([tabname,index,title],()=>{
handleChange()
})
watch(title,()=>{
handleValue()
})