HarmonyOS开发中常用请求取消与并发:AbortController
【摘要】 请求取消与并发:AbortController掌控请求的生命周期,告别资源泄漏 一、背景与动机:为什么需要取消请求?你有没有遇到过这种场景:场景1:页面快速切换用户快速点击"商品列表" → “商品详情” → “商品列表” → “商品详情”…每次切换都发起请求,但旧请求还在进行中。结果:页面数据闪烁,甚至显示旧数据覆盖新数据。场景2:搜索输入用户输入"鸿蒙",还没输完又改成"鸿蒙开发",又改...
请求取消与并发:AbortController
掌控请求的生命周期,告别资源泄漏
一、背景与动机:为什么需要取消请求?
你有没有遇到过这种场景:
场景1:页面快速切换
用户快速点击"商品列表" → “商品详情” → “商品列表” → “商品详情”…每次切换都发起请求,但旧请求还在进行中。结果:页面数据闪烁,甚至显示旧数据覆盖新数据。
场景2:搜索输入
用户输入"鸿蒙",还没输完又改成"鸿蒙开发",又改成"鸿蒙开发教程"…每次输入都触发搜索,结果:最后一次搜索还没返回,上一次的结果先返回了,显示错误数据。
场景3:组件卸载
组件已经销毁了,但请求还在进行。请求返回后尝试更新已销毁组件的状态,导致报错甚至崩溃。
这些问题的根源都是:请求没有取消机制。
AbortController 能解决什么?
- 主动取消:用户主动取消请求(如点击"取消"按钮)
- 自动取消:页面切换时自动取消未完成的请求
- 竞态处理:新请求发出时取消旧请求,避免数据错乱
- 资源释放:及时释放网络资源,避免浪费
- 超时控制:结合超时机制,强制取消长时间未响应的请求
二、核心原理: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 中清理状态
下一篇我们深入数据缓存策略,看看如何让网络请求更高效。
💡 最佳实践提示:建议封装一个
useRequestHook,自动处理取消、loading、error 状态,避免每个页面都写重复代码。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)