在做项目的时候,经常会遇到一些「看似理所当然」的问题。最近我就在写一个模块时,被一个小问题坑了半天:点一下按钮,居然发了两次请求 🤯。
来,带大家复盘一下我是怎么踩坑、怎么排查,最后又是怎么解决的。
背景
场景很简单:页面里展示了一些markdown文本的信息,我需要把其中的电话号码识别出来,并在后面加个「📞 一键呼出」按钮,方便点击拨打。
于是我写了个 useEffect
,在 document
上监听全局点击事件:
erlang
useEffect(() => {
console.log("注册一键呼出事件");
const handleButtonClick = (event: any) => {
if (event.target.classList.contains("phone-call")) {
event.preventDefault();
event.stopPropagation();
const phoneNumber = event.target.getAttribute("data-phone");
if (phoneNumber) {
handlePhoneCall(phoneNumber);
}
}
};
document.addEventListener("click", handleButtonClick);
return () => document.removeEventListener("click", handleButtonClick);
}, []);
按钮是这样插入到内容里的:
bash
供应商电话:138****5678
<button type="button" class="phone-call" data-phone="138****5678">📞一键呼出</button>
结果呢?点一次按钮,触发两次请求!
排查过程
1. 怀疑是事件冒泡
我的第一反应是:是不是点在按钮里的 emoji 也触发了一次?结果打印了一下 event.target
,发现并没有,点按钮或者 emoji,都会只触发一次。
2. 怀疑是事件重复绑定
继续排查下去,发现和 React 18 的 StrictMode 有关。
👉 在开发模式下,StrictMode 会让 useEffect
的副作用执行两次: mount → unmount → mount。
这意味着我的 document.addEventListener
实际上被绑定了两次。 于是点击按钮就触发了两次。
✅ 问题找到了。
解决方案
方案一:用 useCallback
固定函数引用
我第一步尝试是把 handleButtonClick
用 useCallback
包起来,避免清理不掉的问题。
ini
const handleButtonClick = useCallback((event: MouseEvent) => {
const target = (event.target as HTMLElement).closest(".phone-call");
if (target) {
event.preventDefault();
event.stopPropagation();
const phoneNumber = target.getAttribute("data-phone");
if (phoneNumber) {
handlePhoneCall(phoneNumber);
}
}
}, []);
这样能缓解一点,但 StrictMode 还是会执行两次,体验不算完美。
方案二:事件代理到组件容器(最终 ✅)
最终我选择了更彻底的方案 ------ 事件代理。
不在 document
上绑全局事件,而是直接在组件最外层的容器(maven-scroll
)上绑定 onClick
,利用冒泡去捕获按钮点击:
ini
<div
className="maven-scroll"
onClick={(e) => {
const target = (e.target as HTMLElement).closest(".phone-call");
if (target) {
e.preventDefault();
e.stopPropagation();
const phoneNumber = target.getAttribute("data-phone");
if (phoneNumber) {
handlePhoneCall(phoneNumber);
}
}
}}
>
<MdContent mdContent={preprocessContent(supplierBaseReport?.companyInfo)} />
</div>
这样一来:
- 点击按钮 → 被捕获,触发一次 ✅
- 点击容器空白处 →
closest
找不到,啥都不发生 - 不用担心 StrictMode 下的
useEffect
双执行
完美解决 🎉。
最终效果
-
内容会被正则处理,手机号后面自动加按钮:
bash供应商电话:138****5678 <button type="button" class="phone-call" data-phone="138****5678">📞一键呼出</button>
-
点击按钮时,日志只输出一次
总结
- React 18 严格模式会导致副作用执行两次,如果你在副作用里做了事件绑定,要特别注意是否重复。
- 在类似场景下,事件代理比全局事件监听更优雅,逻辑也更可控。
- 用
closest('.phone-call')
能确保即使点到按钮里的 emoji 或文字,也能正确识别。 - 别忘了给动态生成的
<button>
加上type="button"
,避免表单里被当成提交按钮。