HarmonyOS开发中常用请求取消与并发:AbortController

举报
Jack20 发表于 2026/06/25 21:27:42 2026/06/25
【摘要】 请求取消与并发:AbortController掌控请求的生命周期,告别资源泄漏 一、背景与动机:为什么需要取消请求?你有没有遇到过这种场景:场景1:页面快速切换用户快速点击"商品列表" → “商品详情” → “商品列表” → “商品详情”…每次切换都发起请求,但旧请求还在进行中。结果:页面数据闪烁,甚至显示旧数据覆盖新数据。场景2:搜索输入用户输入"鸿蒙",还没输完又改成"鸿蒙开发",又改...

请求取消与并发:AbortController

掌控请求的生命周期,告别资源泄漏

一、背景与动机:为什么需要取消请求?

你有没有遇到过这种场景:

场景1:页面快速切换
用户快速点击"商品列表" → “商品详情” → “商品列表” → “商品详情”…每次切换都发起请求,但旧请求还在进行中。结果:页面数据闪烁,甚至显示旧数据覆盖新数据。

场景2:搜索输入
用户输入"鸿蒙",还没输完又改成"鸿蒙开发",又改成"鸿蒙开发教程"…每次输入都触发搜索,结果:最后一次搜索还没返回,上一次的结果先返回了,显示错误数据。

场景3:组件卸载
组件已经销毁了,但请求还在进行。请求返回后尝试更新已销毁组件的状态,导致报错甚至崩溃。

这些问题的根源都是:请求没有取消机制

AbortController 能解决什么?

  1. 主动取消:用户主动取消请求(如点击"取消"按钮)
  2. 自动取消:页面切换时自动取消未完成的请求
  3. 竞态处理:新请求发出时取消旧请求,避免数据错乱
  4. 资源释放:及时释放网络资源,避免浪费
  5. 超时控制:结合超时机制,强制取消长时间未响应的请求

二、核心原理:AbortController 机制

AbortController 是 Web 标准的取消机制,HarmonyOS 也支持:

flowchart TB
    A[创建AbortController] --> B[获取AbortSignal]
    B --> C[将signal传入请求]
    C --> D[请求进行中]
    
    D --> E{触发取消}
    E -->|调用abort| F[取消请求]
    E -->|请求完成| G[正常返回]
    
    F --> H[抛出AbortError]
    H --> I[清理资源]
    
    G --> J[处理响应]
    
    subgraph 竞态处理
        K[新请求发出] --> L[取消旧请求]
        L --> M[旧请求抛出AbortError]
        M --> N[新请求正常进行]
    end
    
    classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff
    classDef warning fill:#F5A623,stroke:#C17D10,color:#fff
    classDef error fill:#E74C3C,stroke:#C0392B,color:#fff
    classDef info fill:#7ED321,stroke:#5BA318,color:#fff
    
    class A,B,C,D,G,J primary
    class E,K,L,N info
    class F,H,M error
    class I warning

核心概念

  • AbortController:控制器,用于取消请求
  • AbortSignal:信号对象,传递给请求,监听取消事件
  • abort():取消方法,触发取消
  • AbortError:取消错误,标识请求被取消

三、代码实战:请求取消的五大场景

示例1:基础 AbortController 封装

// network/AbortController.ets

/**
 * AbortController封装
 * HarmonyOS原生支持AbortController,这里做一层封装方便使用
 */
export class RequestAbortController {
  private controller: AbortController;
  private aborted: boolean = false;
  private reason: any = null;

  constructor() {
    this.controller = new AbortController();
  }

  /**
   * 获取AbortSignal
   * 用于传递给请求配置
   */
  get signal(): AbortSignal {
    return this.controller.signal;
  }

  /**
   * 是否已取消
   */
  get isAborted(): boolean {
    return this.aborted || this.controller.signal.aborted;
  }

  /**
   * 取消请求
   * @param reason 取消原因(可选)
   */
  abort(reason?: any): void {
    if (this.aborted) {
      return; // 已经取消,不重复操作
    }

    this.aborted = true;
    this.reason = reason || 'Request aborted';
    this.controller.abort(this.reason);

    console.info(`[AbortController] 请求已取消: ${this.reason}`);
  }

  /**
   * 获取取消原因
   */
  getReason(): any {
    return this.reason;
  }

  /**
   * 重置控制器(用于重试场景)
   */
  reset(): void {
    this.controller = new AbortController();
    this.aborted = false;
    this.reason = null;
  }
}

/**
 * 判断是否为取消错误
 */
export function isAbortError(error: any): boolean {
  return error?.name === 'AbortError' ||
         error?.code === 20 || // HarmonyOS错误码
         error?.message?.includes('abort');
}

示例2:HttpClient 集成 AbortController

// network/HttpClient.ets(扩展)
import http from '@ohos.net.http';
import { RequestConfig, Response } from './types';
import { RequestAbortController, isAbortError } from './AbortController';

export class HttpClient {
  // ... 其他代码

  /**
   * 核心请求方法(支持取消)
   * @param config 请求配置
   * @param abortController 取消控制器
   */
  async request<T>(
    config: RequestConfig,
    abortController?: RequestAbortController
  ): Promise<Response<T>> {
    // 检查是否已取消
    if (abortController?.isAborted) {
      throw new Error('Request already aborted');
    }

    // 合并配置
    const mergedConfig = this.mergeConfig(config);

    // 执行请求拦截器
    let processedConfig = mergedConfig;
    for (const interceptor of this.interceptors) {
      if (interceptor.beforeRequest) {
        processedConfig = await interceptor.beforeRequest(processedConfig);
      }
    }

    // 发起请求
    const httpResponse = await this.executeRequest(
      processedConfig,
      abortController?.signal
    );

    // 执行响应拦截器
    let response: Response<T> = {
      data: httpResponse.result as T,
      status: httpResponse.responseCode,
      headers: httpResponse.header,
      config: processedConfig
    };

    for (const interceptor of this.interceptors) {
      if (interceptor.afterResponse) {
        response = await interceptor.afterResponse(response);
      }
    }

    return response;
  }

  /**
   * 执行实际HTTP请求(支持取消)
   * @private
   */
  private async executeRequest(
    config: RequestConfig,
    signal?: AbortSignal
  ): Promise<http.HttpResponse> {
    const httpRequest = http.createHttp();

    try {
      // 监听取消事件
      if (signal) {
        signal.addEventListener('abort', () => {
          // 取消时销毁HTTP对象
          httpRequest.destroy();
          console.info('[HttpClient] 请求已取消,HTTP对象已销毁');
        });
      }

      const response = await httpRequest.request(config.url, {
        method: this.getMethod(config.method),
        header: config.headers,
        extraData: config.data,
        connectTimeout: config.timeout,
        readTimeout: config.timeout
      });

      return response;
    } catch (error) {
      // 判断是否为取消错误
      if (isAbortError(error)) {
        console.info('[HttpClient] 请求被取消');
        throw new Error('Request aborted');
      }
      throw error;
    } finally {
      // 确保销毁HTTP对象
      httpRequest.destroy();
    }
  }
}

示例3:页面切换自动取消请求

// components/PageRequestManager.ets
import { RequestAbortController } from '../network/AbortController';

/**
 * 页面请求管理器
 * 自动管理页面级别的请求取消
 */
export class PageRequestManager {
  private controllers: Map<string, RequestAbortController> = new Map();
  private pageId: string;

  constructor(pageId: string) {
    this.pageId = pageId;
  }

  /**
   * 创建请求控制器
   * @param requestId 请求ID(用于区分同一页面的多个请求)
   */
  createController(requestId: string): RequestAbortController {
    // 如果已存在,先取消旧请求
    this.cancel(requestId);

    // 创建新控制器
    const controller = new RequestAbortController();
    this.controllers.set(requestId, controller);

    return controller;
  }

  /**
   * 取消指定请求
   */
  cancel(requestId: string, reason?: string): void {
    const controller = this.controllers.get(requestId);
    if (controller) {
      controller.abort(reason || `Request ${requestId} cancelled`);
      this.controllers.delete(requestId);
    }
  }

  /**
   * 取消所有请求
   */
  cancelAll(reason?: string): void {
    console.info(`[PageRequestManager] 取消页面 ${this.pageId} 的所有请求`);

    this.controllers.forEach((controller, requestId) => {
      controller.abort(reason || `Page ${this.pageId} unmounted`);
    });

    this.controllers.clear();
  }

  /**
   * 清理已完成的请求
   */
  cleanup(): void {
    this.controllers.forEach((controller, requestId) => {
      if (controller.isAborted) {
        this.controllers.delete(requestId);
      }
    });
  }
}
// pages/ProductDetailPage.ets
import { PageRequestManager } from '../components/PageRequestManager';
import { ProductAPI } from '../api/ProductAPI';

@Entry
@Component
struct ProductDetailPage {
  @State product: Product | null = null;
  @State loading: boolean = false;

  private requestManager: PageRequestManager = new PageRequestManager('ProductDetail');
  private productApi: ProductAPI = new ProductAPI();
  private productId: string = '';

  aboutToAppear() {
    this.loadProduct();
  }

  /**
   * 页面即将消失,取消所有请求
   */
  aboutToDisappear() {
    this.requestManager.cancelAll('页面关闭');
  }

  /**
   * 加载商品详情
   */
  async loadProduct() {
    this.loading = true;

    // 创建取消控制器
    const abortController = this.requestManager.createController('loadProduct');

    try {
      const response = await this.productApi.getDetail(
        this.productId,
        abortController
      );
      this.product = response.data;
    } catch (error) {
      // 判断是否为取消错误
      if (error.message === 'Request aborted') {
        console.info('请求已取消,不显示错误');
        return;
      }
      // 处理其他错误
      console.error('加载失败:', error);
    } finally {
      this.loading = false;
    }
  }

  build() {
    Column() {
      if (this.loading) {
        LoadingProgress()
      } else if (this.product) {
        // 显示商品详情
        this.productContent();
      }
    }
  }

  @Builder
  productContent() {
    Column() {
      Image(this.product?.image)
        .width('100%')
        .height(300)

      Text(this.product?.name || '')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 16 })

      Text(`¥${this.product?.price || 0}`)
        .fontSize(20)
        .fontColor('#E74C3C')
        .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
  }
}

示例4:搜索输入防抖 + 竞态处理

// components/SearchInput.ets
import { RequestAbortController } from '../network/AbortController';

/**
 * 搜索输入组件
 * 功能:
 * 1. 输入防抖
 * 2. 竞态处理(新请求取消旧请求)
 * 3. 取消错误静默处理
 */
@Component
export struct SearchInput {
  @Link searchText: string;
  @Link searchResults: SearchResult[];
  @Link searching: boolean;

  private searchApi: SearchAPI = new SearchAPI();
  private abortController: RequestAbortController | null = null;
  private debounceTimer: number = -1;

  /**
   * 执行搜索
   */
  private async doSearch(keyword: string) {
    // 取消旧请求
    if (this.abortController) {
      this.abortController.abort('新搜索请求');
    }

    // 创建新的取消控制器
    this.abortController = new RequestAbortController();

    this.searching = true;

    try {
      const response = await this.searchApi.search(
        keyword,
        this.abortController
      );
      this.searchResults = response.data;
    } catch (error) {
      // 取消错误静默处理
      if (error.message === 'Request aborted') {
        return;
      }
      console.error('搜索失败:', error);
      this.searchResults = [];
    } finally {
      this.searching = false;
    }
  }

  /**
   * 输入变化处理(防抖)
   */
  private onInputChange(value: string) {
    this.searchText = value;

    // 清除旧定时器
    if (this.debounceTimer !== -1) {
      clearTimeout(this.debounceTimer);
    }

    // 设置新定时器(300ms防抖)
    this.debounceTimer = setTimeout(() => {
      if (value.trim()) {
        this.doSearch(value.trim());
      } else {
        this.searchResults = [];
      }
    }, 300);
  }

  build() {
    Column() {
      // 搜索输入框
      Row() {
        Image($r('app.media.ic_search'))
          .width(20)
          .height(20)
          .margin({ left: 12, right: 8 })

        TextInput({ placeholder: '搜索...', text: this.searchText })
          .layoutWeight(1)
          .height(40)
          .backgroundColor(Color.Transparent)
          .onChange((value) => {
            this.onInputChange(value);
          })

        // 清除按钮
        if (this.searchText) {
          Image($r('app.media.ic_clear'))
            .width(20)
            .height(20)
            .margin({ right: 12 })
            .onClick(() => {
              this.searchText = '';
              this.searchResults = [];
              // 取消正在进行的搜索
              if (this.abortController) {
                this.abortController.abort('清除搜索');
              }
            })
        }
      }
      .width('100%')
      .height(48)
      .backgroundColor('#F5F5F5')
      .borderRadius(24)

      // 搜索结果
      if (this.searching) {
        Row() {
          LoadingProgress()
            .width(24)
            .height(24)
          Text('搜索中...')
            .fontSize(14)
            .fontColor('#999999')
            .margin({ left: 8 })
        }
        .width('100%')
        .height(60)
        .justifyContent(FlexAlign.Center)
      } else if (this.searchResults.length > 0) {
        List() {
          ForEach(this.searchResults, (item: SearchResult) => {
            ListItem() {
              this.searchResultItem(item);
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  searchResultItem(item: SearchResult) {
    Row() {
      Text(item.title)
        .fontSize(16)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .layoutWeight(1)
    }
    .width('100%')
    .height(50)
    .padding({ left: 16, right: 16 })
  }
}

示例5:并发请求管理

// network/ConcurrentRequestManager.ets
import { RequestAbortController } from './AbortController';

/**
 * 并发请求管理器
 * 功能:
 * 1. 管理多个并发请求
 * 2. 支持全部取消
 * 3. 支持等待全部完成
 */
export class ConcurrentRequestManager {
  private requests: Map<string, {
    controller: RequestAbortController;
    promise: Promise<any>;
  }> = new Map();

  /**
   * 添加并发请求
   * @param id 请求ID
   * @param requestFn 请求函数
   */
  async add<T>(id: string, requestFn: (signal: AbortSignal) => Promise<T>): Promise<T> {
    // 取消同ID的旧请求
    this.cancel(id);

    // 创建新控制器
    const controller = new RequestAbortController();

    // 执行请求
    const promise = requestFn(controller.signal);

    // 记录请求
    this.requests.set(id, { controller, promise });

    try {
      const result = await promise;
      // 请求完成,移除记录
      this.requests.delete(id);
      return result;
    } catch (error) {
      this.requests.delete(id);
      throw error;
    }
  }

  /**
   * 取消指定请求
   */
  cancel(id: string, reason?: string): void {
    const request = this.requests.get(id);
    if (request) {
      request.controller.abort(reason || `Request ${id} cancelled`);
      this.requests.delete(id);
    }
  }

  /**
   * 取消所有请求
   */
  cancelAll(reason?: string): void {
    this.requests.forEach((request, id) => {
      request.controller.abort(reason || 'All requests cancelled');
    });
    this.requests.clear();
  }

  /**
   * 等待所有请求完成
   */
  async allSettled(): Promise<PromiseSettledResult<any>[]> {
    const promises = Array.from(this.requests.values()).map(r => r.promise);
    return Promise.allSettled(promises);
  }

  /**
   * 获取正在进行的请求数量
   */
  get pendingCount(): number {
    return this.requests.size;
  }
}
// 使用示例:批量请求商品详情
async function loadProductDetails(ids: string[]) {
  const manager = new ConcurrentRequestManager();
  const productApi = new ProductAPI();

  // 并发请求所有商品详情
  const promises = ids.map(id => 
    manager.add(`product-${id}`, (signal) => 
      productApi.getDetail(id, { signal })
    )
  );

  try {
    // 等待所有请求完成
    const results = await Promise.allSettled(promises);
    
    // 处理结果
    const products: Product[] = [];
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        products.push(result.value.data);
      } else {
        console.error(`商品 ${ids[index]} 加载失败:`, result.reason);
      }
    });

    return products;
  } catch (error) {
    // 发生错误,取消所有请求
    manager.cancelAll('批量请求失败');
    throw error;
  }
}

四、踩坑与注意事项

坑1:取消后忘记清理状态

问题:请求取消了,但 loading 状态没重置,页面一直显示加载中。

// ❌ 错误:取消后loading没重置
try {
  this.loading = true;
  await api.getData(abortController);
  this.loading = false;
} catch (e) {
  // 取消错误没处理,loading一直是true
}

// ✅ 正确:finally中重置
try {
  this.loading = true;
  await api.getData(abortController);
} catch (e) {
  if (e.message === 'Request aborted') {
    return; // 静默处理
  }
  // 处理其他错误
} finally {
  this.loading = false; // 无论成功失败取消,都重置
}

坑2:取消错误被当作普通错误处理

问题:取消错误触发了错误提示,用户看到"请求失败"。

// ❌ 错误:取消错误也显示Toast
catch (e) {
  showToast('请求失败');
}

// ✅ 正确:区分取消错误
catch (e) {
  if (isAbortError(e)) {
    return; // 取消错误静默处理
  }
  showToast('请求失败');
}

坑3:AbortController 重复使用

问题:一个 AbortController 用于多个请求,取消时全部被取消。

// ❌ 错误:共用一个controller
const controller = new AbortController();
await api.getA(controller.signal);
await api.getB(controller.signal); // 如果A取消,B也被取消

// ✅ 正确:每个请求独立controller
const controllerA = new AbortController();
const controllerB = new AbortController();
await api.getA(controllerA.signal);
await api.getB(controllerB.signal);

坑4:组件卸载后更新状态

问题:请求返回时组件已卸载,更新状态报错。

// ❌ 错误:组件卸载后还更新状态
aboutToDisappear() {
  // 没取消请求
}

async loadData() {
  const data = await api.getData();
  this.data = data; // 组件已卸载,报错
}

// ✅ 正确:组件卸载时取消请求
private abortController: RequestAbortController;

aboutToDisappear() {
  this.abortController?.abort('组件卸载');
}

async loadData() {
  this.abortController = new RequestAbortController();
  const data = await api.getData(this.abortController.signal);
  this.data = data;
}

五、HarmonyOS 6 适配要点

1. AbortController 原生支持

// HarmonyOS 6 完整支持AbortController
const controller = new AbortController();

// 请求配置中传入signal
httpRequest.request(url, {
  method: http.RequestMethod.GET,
  // ✨ HarmonyOS 6 支持signal
  signal: controller.signal
});

// 取消请求
controller.abort('用户取消');

2. HTTP请求取消增强

// HarmonyOS 6 支持更优雅的取消
const httpRequest = http.createHttp();

// 方式1:使用AbortController
const controller = new AbortController();
httpRequest.request(url, { signal: controller.signal });

// 方式2:直接销毁(HarmonyOS 5也支持)
httpRequest.destroy();

// 方式3:取消特定请求(HarmonyOS 6新增)
httpRequest.cancel(); // 更优雅的取消方法

3. 错误码标准化

// HarmonyOS 6 定义了取消错误码
// 错误码:2300018 - 用户取消

if (error.code === 2300018) {
  console.info('请求被用户取消');
}

六、总结

请求取消是网络层的"刹车系统",让请求可控、可预测:

场景 取消时机 处理方式
页面切换 aboutToDisappear 取消所有请求
搜索输入 新请求发出 取消旧请求
组件卸载 aboutToDisappear 取消组件请求
用户主动 点击取消按钮 取消当前请求
超时强制 超时后 取消请求并报错

记住几个原则:

  • ✅ 每个请求都支持取消
  • ✅ 取消错误静默处理
  • ✅ 组件卸载必须取消
  • ✅ 竞态场景主动取消旧请求
  • ✅ finally 中清理状态

下一篇我们深入数据缓存策略,看看如何让网络请求更高效。


💡 最佳实践提示:建议封装一个 useRequest Hook,自动处理取消、loading、error 状态,避免每个页面都写重复代码。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。