React中setInterval运行时fetch请求无法送达服务端问题排查
React 倒计时运行时接口请求无法发送问题排查
问题现象
- 项目包含两类核心逻辑:
- 倒计时逻辑:调用
setInterval()实现每秒秒数减1的4分钟倒计时,计数归零时调用clearInterval()终止计时,同时调用解锁座位的接口 - 购票逻辑:购票按钮通过
onClick绑定事件,触发时向Node.js服务端发送购票接口请求
- 倒计时逻辑:调用
- 预期表现:倒计时运行期间点击购票按钮,倒计时正常递减的同时,购票请求可以正常到达服务端
- 实际表现:点击按钮后无请求到达服务端;已通过Postman验证服务端连通性、接口路由配置正常,通过
alert()确认onClick事件可以正常触发绑定函数,初步怀疑和JavaScript单线程机制相关
相关实现代码
倒计时启动逻辑
// 启动计时器的组件逻辑 const startTimer = (sitNumber) => { const id = setInterval(async () => { updateSec = updateSec - 1; console.log("updateSec", updateSec); console.log("minutes", time.m); if (updateSec === 0) { updateMin = updateMin - 1; updateSec = 60; } if (time.m <= 0 || updateMin <= 0) { clearInterval(id); const response = await fetch( "http://localhost:3005/dashboard/unblocksit", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ flightName: flight.flightName, sit: sitNumber, }), } ); const parseResponse = await response.json(); setCount2(count + 1); setShowCheckOut(false); setTime({ s: 60, m: 4 }); console.log("this is the unblock response", parseResponse); } return setTime({ s: updateSec, m: updateMin }); }, 1000); };
购票按钮逻辑
// 发送购票请求的组件逻辑 const buyTicket = async (e) => { e.preventDefault(); // alert('button clicked') // 弹窗可以正常触发 try { // 该接口在Postman中可以正常访问 const response = await fetch("http://localhost:3005/dashboard/buyticket", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ flight: flight.flightName, sit: sit.sitNumber, price: sit.price, origin: flight.origin, destination: flight.destination, passenger, }), }); const parseResponse = response.json(); console.log(parseResponse); setCount3(count3 + 1); } catch (error) { console.log(error); } }; return ( <div> <div className="timer"> <span>{time.m <= 0 ? time.m : "0" + time.m}</span> <span>{time.s <= 0 ? time.s : "0" + time.s}</span> </div> <div> <form> <label>Passenger full name</label> <input type="text" value={passenger} onChange={(e) => setPassenger(e.target.value)} /> <button onClick={buyTicket}>Purchase</button> </form> </div> </div> );
服务端购票接口代码
// 服务端buyTicket控制器逻辑 buyTicket: async (req, res) => { console.log("hello from buyticket controller"); // 该日志始终未打印 const flight = req.body.flightName; const sit = req.body.sitNumber; const price = req.body.price; const origin = req.body.origin; const detination = req.body.detination; const passenger = req.body.passenger; const sitUnavailable = await Flight.findOneAndUpdate( { flightName: flight }, { $set: { "sits.$[s].isAvailable": false, "sits.$[s].isSuspended": false }, }, { arrayFilters: [{ "sits.sitNumber": sit }], new: true } ); return res.send(sitUnavailable); };
根因分析
问题核心是定时器管理错误导致主线程阻塞:
- 倒计时启动逻辑没有做生命周期管控:如果
startTimer直接写在组件函数顶层,每次组件因为setTime触发重渲染时,都会新建一个setInterval定时器,旧的定时器没有被清理,会持续驻留 - 定时器数量会指数级增长:第1秒1个定时器触发,执行后触发重渲染新增1个定时器;第2秒2个定时器同时触发,两次重渲染新增2个定时器;运行10秒就会有超过500个定时器回调排队执行,主线程被大量同步计算、日志打印、state更新操作完全占满
- 受JavaScript单线程机制限制,主线程阻塞时,事件循环无法正常调度网络请求相关任务,即使onClick的同步逻辑(比如alert)可以触发,后续的fetch请求也无法被正常发送
另外代码中还存在几个会导致后续异常的隐性问题:
- 倒计时的
updateSec、updateMin使用组件外普通变量存储,会存在闭包陷阱,拿到的状态值不是最新值 setInterval传入async回调时,不会等待内部await执行完成就调度下一次回调,本身就容易出现回调堆积- 购票逻辑中
response.json()缺少await,无法拿到正常的接口返回值 - 服务端购票接口字段取值错误:客户端传的是
flight字段,服务端错误读取req.body.flightName - 时间补零逻辑写反,会导致大于等于10秒/10分的时间显示异常
修复方案
- 用
useEffect管控定时器生命周期,仅在组件挂载时启动一次定时器,组件卸载时清理定时器,避免重复生成:
useEffect(() => { const id = setInterval(() => { // 倒计时逻辑 }, 1000); // 清理函数 return () => clearInterval(id); }, []);
- 倒计时的分、秒值全部用
useState管理,不要用外部普通变量,避免闭包问题 - 建议用递归
setTimeout替代setInterval实现倒计时,避免async回调堆积:
const timerRef = useRef(null); const tick = useCallback(() => { // 执行秒数递减、判断归零逻辑 timerRef.current = setTimeout(tick, 1000); }, [/* 相关状态依赖 */]) useEffect(() => { timerRef.current = setTimeout(tick, 1000); return () => clearTimeout(timerRef.current); }, [tick])
- 给购票逻辑的
response.json()加上await,修正服务端接口字段读取错误 - 修正时间补零逻辑,小于10的数值才补前导零
内容的提问来源于stack exchange,提问作者user16604664
相关产品推荐
相关产品推荐

