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

如何在React/TypeScript中正确取消异步轮询操作?

服务器存活检查的异步取消方案

问题背景

我实现了一个服务器存活检查功能,每10秒轮询一次,最多尝试10次,代码如下:

const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));

export async function checkRemote(
  { maxAttempts = 10 }: { maxAttempts: number } = { maxAttempts: 10 }
) {
  let i = 1;
  while (true) {
    const res = await fetch(`/check`);
    if (res.status >= 400) {
      if (i === maxAttempts) {
        throw new Error("Check failed");
      } else {
        i++;
        await wait(10000);
        continue;
      }
    } else {
      const json = await res.json();
      return json;
    }
  }
}

问题在于,若React组件在轮询过程中卸载,checkRemote函数仍会继续执行。

我想咨询:

  1. 调用checkRemote必须在useEffect中吗?能否通过按钮触发,同时保证组件卸载时可取消操作?
  2. 如何通过AbortController这类工具,将取消逻辑传递到嵌套函数中,实现所有异步操作的正确终止?

注:核心需求是搭建异步取消机制,而非针对该场景使用useContext等方案。


问题解答

1. 调用checkRemote不必局限于useEffect,按钮触发也能实现卸载时取消

完全可以通过按钮触发checkRemote,只要在组件内维护AbortController实例管理取消信号,并在组件卸载时调用abort()即可终止所有未完成的异步操作。

具体实现示例:

import { useEffect, useRef } from 'react';
import { checkRemote } from './your-file-path';

function ServerCheckComponent() {
  const abortControllerRef = useRef<AbortController | null>(null);

  // 组件卸载时自动取消所有操作
  useEffect(() => {
    return () => abortControllerRef.current?.abort();
  }, []);

  const handleStartCheck = async () => {
    // 先取消上一次未完成的检查(如果存在)
    abortControllerRef.current?.abort();
    
    const controller = new AbortController();
    abortControllerRef.current = controller;

    try {
      const result = await checkRemote({ maxAttempts: 10 }, controller.signal);
      console.log('服务器检查成功:', result);
    } catch (err) {
      // 过滤取消操作的错误,避免控制台冗余报错
      if (!(err instanceof DOMException && err.name === 'AbortError')) {
        console.error('服务器检查失败:', err);
      }
    } finally {
      abortControllerRef.current = null;
    }
  };

  return <button onClick={handleStartCheck}>启动服务器存活检查</button>;
}

2. 通过AbortController传递取消信号,终止所有异步操作

要让checkRemote及内部的异步操作响应取消,需要对wait函数和checkRemote函数做针对性改造:

第一步:改造wait函数支持取消信号

const wait = (ms: number, signal?: AbortSignal) => {
  return new Promise<void>((resolve, reject) => {
    const timer = setTimeout(resolve, ms);

    // 若信号已中止,直接清理定时器并拒绝Promise
    if (signal?.aborted) {
      clearTimeout(timer);
      reject(new DOMException('Wait aborted', 'AbortError'));
      return;
    }

    // 监听中止事件,触发时清理定时器并拒绝
    signal?.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(new DOMException('Wait aborted', 'AbortError'));
    });
  });
};

第二步:改造checkRemote函数接收并传递取消信号

export async function checkRemote(
  { maxAttempts = 10 }: { maxAttempts: number } = { maxAttempts: 10 },
  signal?: AbortSignal
) {
  // 先检查信号是否已中止,避免无效执行
  if (signal?.aborted) {
    throw new DOMException('Check aborted', 'AbortError');
  }

  let i = 1;
  while (true) {
    // 把信号传递给fetch,原生支持中止请求
    const res = await fetch(`/check`, { signal });
    
    if (res.status >= 400) {
      if (i === maxAttempts) {
        throw new Error("Check failed");
      } else {
        i++;
        // 等待时也传递信号,确保轮询间隔也能被中止
        await wait(10000, signal);
        continue;
      }
    } else {
      return res.json();
    }
  }
}

关键逻辑说明

  • fetch原生支持signal参数,调用abort()会直接终止请求并抛出AbortError
  • 自定义wait函数通过监听abort事件,确保轮询的等待过程也能被即时终止
  • 组件中用useRef保存AbortController实例,避免组件重渲染丢失引用,同时支持多次点击时自动取消上一次操作

内容的提问来源于stack exchange,提问作者Lance Pollard

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 06:23:19