这是一个非常经典的**抢购/秒杀**场景的前端优化问题。
### 结论:
1. **防抖(Debounce)绝对不适合**:防抖的机制是“等你疯狂点击停下来之后,才执行一次”。在抢购场景下,用户一定会疯狂连点,如果用防抖,用户的请求会一直被延迟,直到他点累了停下来才会发请求,那时候黄花菜都凉了。
2. **节流(Throttle)适用,但不够完美**:节流是“点第一下立刻执行,然后接下来的 1 秒内无论怎么点都忽略”。这能防止前端发送过多请求,但是如果接口响应很慢(比如超过 1 秒),1 秒后用户再点,又会发出一个重复请求。
3. **最适合的方案:异步状态锁(Loading 锁)+ UI 禁用**:核心逻辑是“**在当前接口没有返回结果之前,拒绝一切新的请求**”。
### 优化方案:给请求加上“异步锁”
相比于单纯依赖时间的节流,使用变量锁(推荐用 `useRef` 以避免重新渲染带来的闭包问题)来判断当前是否正在请求中,是最稳妥的。
我可以帮你修改 `handleLotteryClick`,引入一个 `lockRef`:
```tsx
// 使用 useRef 作为一个不受渲染周期影响的同步锁
const lockRef = useRef(false);
const handleLotteryClick = useCallback(
async (item: MemberDayFlashSaleItem) => {
// 1. 如果当前已经被锁住(说明上一个请求还没回来),直接拦截,什么都不做
if (lockRef.current) return;
// 2. 没上锁的话,立马加锁
lockRef.current = true;
const timestamp = Math.floor(Date.now() / 1000);
try {
const res = await claimMemberDayFlashSale({
activity_id: Number(item.activity_id),
session_id: Number(item.session_id),
welfare_id: Number(item.welfare_id),
timestamp: timestamp,
sign: buildMemberDayFlashSaleClaimSign({
activity_id: Number(item.activity_id),
session_id: Number(item.session_id),
welfare_id: Number(item.welfare_id),
timestamp: timestamp,
}),
});
if (res.code === 0) {
setPrizePopVisible(true);
setWinningPrizeInfo(item);
} else {
showToast(res.message || '活动火爆,请稍后尝试');
}
} catch (err) {
showToast('活动火爆,请稍后尝试');
} finally {
// 3. 无论成功还是失败,最后一定要把锁解开,允许下一次点击
// 如果想限制得更死,这里可以加个 setTimeout(..., 1000) 强制 1 秒后才能再次点击
lockRef.current = false;
}
},
[]
);
```
### 进阶优化(针对极致抢购场景):
如果在极端火爆的场景下,为了保护后端,我们可以把 **异步锁** 和 **节流** 结合起来。
在 `finally` 中不立刻解锁,而是强制等待一定时间(比如 500ms),也就是所谓的**强制冷却 CD**:
```tsx
} finally {
// 请求结束后,强制冷却 500ms 再解锁,相当于变相的节流
setTimeout(() => {
lockRef.current = false;
}, 500);
}
```
