You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),
    ])
解决方案

核心思路

  1. 用AbortController终止未完成请求:浏览器原生API可直接中断fetch请求,避免无效网络资源占用。
  2. 标记任务完成状态:设置共享状态,当一个请求完成后,另一个请求即使返回结果也不再更新UI。
  3. 重构异步函数:让函数接收终止信号和状态检查逻辑,确保中断后不执行后续操作。

具体实现步骤

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 04:45:42