React中如何在一个函数返回数据后终止另一个异步函数
问题描述
我有两个异步函数,分别调用不同接口拉取数据,有时其中一个返回速度更快。这两个函数通过onClick事件同时触发,返回的数据用来填充表格——只要有一个表格填充完成,就不需要再处理较慢的那个函数的结果。
我试过用return退出函数,但不知道怎么在一个函数返回后终止另一个;串行调用会增加等待时间,不可行;用Promise.race的话,发现慢的函数还在运行,而且返回值是undefined,求可行方案。
相关代码如下:
// 示例函数 const getDataOne = async => { await fetch('//some-api/one').then(res => res.json()).then(data => { setDataOne(date) }).catch(err => console.log(err)) } const getDataTwo = async => { await fetch('//some-api/two').then(res => res.json()).then(data => { setDataTwo(date) }).catch(err => console.log(err)) } // 实际使用Promise.race的代码 const getThirdPartyLiveRampSegments = async (date, token) => { // resets setThirdPartyLiveRampSegments setThirdPartyLiveRampSegments([]) setThirdPartyLiveRampSegmentsLoading(true) let count = 0 const timer = setInterval(() => { count++ setThirdPartyLiveRampSegmentsCount(count) }, 1000) if (token) { try { await fetch( `//${ import.meta.env.VITE_BE_HOST }/api/thirdparty-liveramp-segments?date=${date}&token=${token}`, ) .then(res => res.json()) .then(data => { setThirdPartyLiveRampSegments(data) setThirdPartyLiveRampSegmentsLoading(false) // clears token input to reuse with date input setToken('') // clear timer clearInterval(timer) }) } catch (error) { console.log(error) } } else { setThirdPartyLiveRampSegmentsLoading(false) popupValidation('error', 'Please enter a segment id') } } const getportalDboAdvpixel = async token => { // resets setportalDboAdvpixel setportalDboAdvpixel([]) setportalDboAdvpixelLoading(true) let count = 0 const timer = setInterval(() => { count++ setportalDboAdvpixelCount(count) }, 1000) if (token) { try { await fetch( `//${ import.meta.env.VITE_BE_HOST }/api/portal-dbo-advpixel?token=${token}`, ) .then(res => res.json()) .then(data => { setportalDboAdvpixel(data) setportalDboAdvpixelLoading(false) // clear timer clearInterval(timer) }) } catch (error) { console.log(error) } } else { setportalDboAdvpixelLoading(false) popupValidation('error', 'Please enter a segment id') } } const getSegmentDataRace = async (date, token) => await Promise.race([ getThirdPartyLiveRampSegments(date, token), getportalDboAdvpixel(token), ])
解决方案
核心思路
- 用
AbortController终止未完成请求:浏览器原生API可直接中断fetch请求,避免无效网络资源占用。 - 标记任务完成状态:设置共享状态,当一个请求完成后,另一个请求即使返回结果也不再更新UI。
- 重构异步函数:让函数接收终止信号和状态检查逻辑,确保中断后不执行后续操作。
具体实现步骤
1. 定义共享状态与控制器
在组件内部(以React为例)初始化状态和控制器存储:
import { useState, useRef, useEffect } from 'react'; // 标记是否已有请求完成 const [taskCompleted, setTaskCompleted] = useState(false); // 存储AbortController实例,用于终止请求 const abortControllers = useRef([]);
2. 重构异步函数,支持中断与状态判断
修改两个请求函数,新增中断信号和状态检查参数:
const getThirdPartyLiveRampSegments = async (date, token, signal, checkCompleted) => { // 已有任务完成则直接退出 if (checkCompleted()) return; setThirdPartyLiveRampSegments([]); setThirdPartyLiveRampSegmentsLoading(true); let count = 0; const timer = setInterval(() => { count++; setThirdPartyLiveRampSegmentsCount(count); }, 1000); if (token) { try { const res = await fetch( `//${import.meta.env.VITE_BE_HOST}/api/thirdparty-liveramp-segments?date=${date}&token=${token}`, { signal } // 传入中断信号 ); // 再次检查,避免竞态问题 if (checkCompleted()) { clearInterval(timer); setThirdPartyLiveRampSegmentsLoading(false); return; } const data = await res.json(); // 仅当无任务完成时更新UI if (!checkCompleted()) { setThirdPartyLiveRampSegments(data); setThirdPartyLiveRampSegmentsLoading(false); setToken(''); setTaskCompleted(true); } clearInterval(timer); } catch (error) { // 忽略主动中断的请求错误 if (error.name !== 'AbortError') { console.log(error); setThirdPartyLiveRampSegmentsLoading(false); } clearInterval(timer); } } else { setThirdPartyLiveRampSegmentsLoading(false); popupValidation('error', 'Please enter a segment id'); } }; const getportalDboAdvpixel = async (token, signal, checkCompleted) => { if (checkCompleted()) return; setportalDboAdvpixel([]); setportalDboAdvpixelLoading(true); let count = 0; const timer = setInterval(() => { count++; setportalDboAdvpixelCount(count); }, 1000); if (token) { try { const res = await fetch( `//${import.meta.env.VITE_BE_HOST}/api/portal-dbo-advpixel?token=${token}`, { signal } ); if (checkCompleted()) { clearInterval(timer); setportalDboAdvpixelLoading(false); return; } const data = await res.json(); if (!checkCompleted()) { setportalDboAdvpixel(data); setportalDboAdvpixelLoading(false); setTaskCompleted(true); } clearInterval(timer); } catch (error) { if (error.name !== 'AbortError') { console.log(error); setportalDboAdvpixelLoading(false); } clearInterval(timer); } } else { setportalDboAdvpixelLoading(false); popupValidation('error', 'Please enter a segment id'); } };
3. 重构触发函数,管理控制器与状态
const getSegmentDataRace = async (date, token) => { // 重置状态与之前的请求 setTaskCompleted(false); abortControllers.current.forEach(controller => controller.abort()); abortControllers.current = []; // 创建两个中断控制器 const controller1 = new AbortController(); const controller2 = new AbortController(); abortControllers.current.push(controller1, controller2); // 定义状态检查函数 const checkCompleted = () => taskCompleted; // 同时发起请求 const promise1 = getThirdPartyLiveRampSegments(date, token, controller1.signal, checkCompleted); const promise2 = getportalDboAdvpixel(token, controller2.signal, checkCompleted); // 等待第一个完成的请求 await Promise.race([promise1, promise2]); // 终止剩余请求 abortControllers.current.forEach(controller => controller.abort()); };
4. 组件卸载时清理请求
useEffect(() => { return () => { abortControllers.current.forEach(controller => controller.abort()); }; }, []);
关键说明
AbortController:直接中断未完成的fetch请求,减少无效网络开销。- 多次状态检查:在请求返回后、更新UI前校验状态,避免慢请求覆盖快请求结果的竞态问题。
- 重置逻辑:每次触发请求时先清理之前的请求和状态,确保不受历史请求影响。
内容的提问来源于stack exchange,提问作者mrpbennett
相关产品推荐
相关产品推荐

