鸿蒙异步处理从入门到实战:Promise、async/await、并发池、超时重试全套攻略

摘要(介绍目前的背景和现状)

在鸿蒙(HarmonyOS)里,网络请求、文件操作、数据库访问这类 I/O 都是异步的。主流写法跟前端类似:Promiseasync/await、回调。想把 app 做得"流畅且不阻塞",核心在于:合理用 async/await、把错误兜住、并发别乱开、遇到慢接口要有"超时/取消/重试"机制。

引言(介绍目前的发展情况和场景应用)

从 API 设计上看,鸿蒙的系统能力(比如 @ohos.net.http@ohos.file.fs@ohos.webSocket 等)都提供了 Promise 风格的方法。我们在实际项目里,通常会封一层"请求工具模块",加上统一的超时、重试、拦截日志,然后在页面中以 async/await 的写法去调用。本文用一个小 Demo 把这些串起来:页面里点几个按钮,分别触发请求、并发、取消、文件读写等操作,看得到结果和错误提示。

异步基础与风格选型

Promise 链式 vs. async/await

  • 链式写法好处是可控的组合then/catch/finally),缺点是可读性差
  • async/await 写起来更像同步代码,可读性强 ,但别忘了 try/catch
  • 两者可以混用:底层工具用 Promise 封装,业务层面用 async/await 调用。

代码示例:Promise 链式 & async/await 对比

ts 复制代码
import http from '@ohos.net.http';

// 链式写法
function fetchTodoByIdChained(id: number) {
  const client = http.createHttp();
  return client.request(`https://jsonplaceholder.typicode.com/todos/${id}`, {
    method: http.RequestMethod.GET,
    connectTimeout: 5000,
    readTimeout: 5000,
  })
  .then(res => JSON.parse(res.result as string))
  .finally(() => client.destroy());
}

// async/await 写法
async function fetchTodoByIdAsync(id: number) {
  const client = http.createHttp();
  try {
    const res = await client.request(`https://jsonplaceholder.typicode.com/todos/${id}`, {
      method: http.RequestMethod.GET,
      connectTimeout: 5000,
      readTimeout: 5000,
    });
    return JSON.parse(res.result as string);
  } finally {
    client.destroy();
  }
}

一个可运行的 Demo(页面 + 工具模块)

结构建议:
features/async/AsyncDemoPage.ets(页面)
features/async/asyncKit.ts(工具模块:超时、取消、重试、并发池、文件读写)

工具模块:asyncKit.ts

ts 复制代码
// features/async/asyncKit.ts
import http from '@ohos.net.http';
import fs from '@ohos.file.fs';

/** 一次 HTTP 请求,带超时控制 */
export async function httpGet<T = unknown>(url: string, timeoutMs = 6000): Promise<T> {
  const client = http.createHttp();
  try {
    const req = client.request(url, {
      method: http.RequestMethod.GET,
      connectTimeout: timeoutMs,
      readTimeout: timeoutMs,
    });
    // 双保险:用 Promise.race 再做一层超时
    const res = await Promise.race([
      req,
      delayReject(timeoutMs, new Error(`Timeout after ${timeoutMs}ms`))
    ]);
    const txt = (res as http.HttpResponse).result as string;
    return JSON.parse(txt) as T;
  } finally {
    client.destroy();
  }
}

/** 延迟 resolve/reject */
export function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}
export function delayReject<T = never>(ms: number, err: Error): Promise<T> {
  return new Promise((_, reject) => setTimeout(() => reject(err), ms));
}

/** 重试:指数退避 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  attempts = 3,
  baseDelayMs = 300
): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      lastErr = e;
      if (i < attempts - 1) {
        const backoff = baseDelayMs * Math.pow(2, i); // 300, 600, 1200...
        await delay(backoff);
      }
    }
  }
  throw lastErr;
}

/** 并发池:限制并发数量,避免把网络或设备打满 */
export async function runWithPool<T>(
  tasks: Array<() => Promise<T>>,
  concurrency = 3
): Promise<T[]> {
  const results: T[] = [];
  let index = 0;
  const workers: Promise<void>[] = [];

  async function worker() {
    while (index < tasks.length) {
      const cur = index++;
      try {
        const r = await tasks[cur]();
        results[cur] = r;
      } catch (e) {
        // 不中断整体:把错误抛出去给上层处理也可
        results[cur] = e as any;
      }
    }
  }

  for (let i = 0; i < Math.min(concurrency, tasks.length); i++) {
    workers.push(worker());
  }
  await Promise.all(workers);
  return results;
}

/** 取消控制(简版):通过外部标记与自定义取消逻辑 */
export class CancelToken {
  private _cancelled = false;
  cancel() { this._cancelled = true; }
  get cancelled() { return this._cancelled; }
}

/** 支持"软取消"的 GET:每步检查 token,早停 */
export async function httpGetCancellable<T = unknown>(
  url: string,
  token: CancelToken,
  timeoutMs = 6000
): Promise<T> {
  if (token.cancelled) throw new Error('Cancelled before start');
  const client = http.createHttp();
  try {
    const req = client.request(url, {
      method: http.RequestMethod.GET,
      connectTimeout: timeoutMs,
      readTimeout: timeoutMs,
    });
    const res = await Promise.race([
      req,
      delayReject(timeoutMs, new Error(`Timeout after ${timeoutMs}ms`))
    ]);
    if (token.cancelled) throw new Error('Cancelled after response');
    const txt = (res as http.HttpResponse).result as string;
    return JSON.parse(txt) as T;
  } finally {
    client.destroy();
  }
}

/** 文件读写示例(Promise 风格) */
export async function writeTextFile(path: string, content: string): Promise<void> {
  const file = await fs.open(path, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
  try {
    await fs.write(file.fd, content);
  } finally {
    await fs.close(file.fd);
  }
}

export async function readTextFile(path: string): Promise<string> {
  const file = await fs.open(path, fs.OpenMode.READ_ONLY);
  try {
    const stat = await fs.stat(path);
    const buf = new ArrayBuffer(stat.size);
    await fs.read(file.fd, buf);
    return String.fromCharCode(...new Uint8Array(buf));
  } finally {
    await fs.close(file.fd);
  }
}

页面:AsyncDemoPage.ets

ts 复制代码
// features/async/AsyncDemoPage.ets
import promptAction from '@ohos.promptAction';
import { httpGet, withRetry, runWithPool, CancelToken, httpGetCancellable, writeTextFile, readTextFile } from './asyncKit';

@Entry
@Component
struct AsyncDemoPage {
  @State log: string = '';
  token: CancelToken = new CancelToken();

  private append(msg: string) {
    this.log = `[${new Date().toLocaleTimeString()}] ${msg}\n` + this.log;
  }

  async onFetchOnce() {
    try {
      const data = await httpGet<any>('https://jsonplaceholder.typicode.com/todos/1', 4000);
      this.append(`单次请求成功:${JSON.stringify(data)}`);
      promptAction.showToast({ message: '请求成功' });
    } catch (e) {
      this.append(`单次请求失败:${(e as Error).message}`);
      promptAction.showToast({ message: '请求失败' });
    }
  }

  async onFetchWithRetry() {
    try {
      const data = await withRetry(() => httpGet<any>('https://jsonplaceholder.typicode.com/todos/2', 2000), 3, 300);
      this.append(`重试成功:${JSON.stringify(data)}`);
    } catch (e) {
      this.append(`重试最终失败:${(e as Error).message}`);
    }
  }

  async onParallelLimited() {
    const ids = Array.from({ length: 8 }, (_, i) => i + 1);
    const tasks = ids.map(id => () => httpGet<any>(`https://jsonplaceholder.typicode.com/todos/${id}`, 5000));
    const results = await runWithPool(tasks, 3);
    const ok = results.filter(r => !(r instanceof Error)).length;
    this.append(`并发池完成:成功 ${ok}/${results.length}`);
  }

  async onCancellable() {
    this.token = new CancelToken();
    const p = httpGetCancellable<any>('https://jsonplaceholder.typicode.com/todos/3', this.token, 6000);
    setTimeout(() => {
      this.token.cancel();
      this.append('已发出取消信号');
    }, 200); // 模拟用户取消
    try {
      const data = await p;
      this.append(`取消示例返回:${JSON.stringify(data)}`);
    } catch (e) {
      this.append(`取消示例结束:${(e as Error).message}`);
    }
  }

  async onFileReadWrite() {
    try {
      const path = `/data/storage/el2/base/files/async_demo.txt`;
      await writeTextFile(path, `hello @ ${Date.now()}`);
      const text = await readTextFile(path);
      this.append(`文件读写成功:${text}`);
    } catch (e) {
      this.append(`文件读写失败:${(e as Error).message}`);
    }
  }

  build() {
    Column() {
      Text('异步请求 Demo').fontSize(22).fontWeight(FontWeight.Bold).margin({ bottom: 8 })
      Row({ space: 8 }) {
        Button('单次请求').onClick(() => this.onFetchOnce())
        Button('重试请求').onClick(() => this.onFetchWithRetry())
        Button('并发池').onClick(() => this.onParallelLimited())
      }
      Row({ space: 8 }) {
        Button('可取消请求').onClick(() => this.onCancellable())
        Button('文件读写').onClick(() => this.onFileReadWrite())
      }.margin({ top: 8 })

      Scroll() {
        Text(this.log).fontSize(14).maxLines(1000)
      }.margin({ top: 12 }).height('60%').width('100%')
    }
    .padding(16)
    .backgroundColor('#FFFFFF')
    .width('100%')
    .height('100%')
  }
}

说明:

  • 页面按钮触发不同的异步策略:单次请求、带重试、并发池、软取消、文件读写。
  • promptAction.showToast 做轻提示;日志区能看到详细过程。
  • "可取消请求"里用自定义 CancelToken 做软取消,避开直接中断底层请求可能带来的不一致。

典型模式与落地细节

超时与取消

代码示例:超时包装 + 软取消要点

  • 超时:Promise.race([真实请求, delayReject(...)])
  • 取消:在关键点检查 token.cancelled,抛错早停,外层统一善后
ts 复制代码
// 见 asyncKit.ts -> httpGet / httpGetCancellable

并发与限流

  • Promise.all 一把梭容易把对端打挂;建议并发池控制并发数(如 3~5 个)。
  • 对失败任务可以部分重试 ,同时采用 allSettled 汇总成功/失败。
ts 复制代码
// 见 asyncKit.ts -> runWithPool

重试与指数退避

  • 固定重试间隔会形成"重试风暴";指数退避(300ms、600ms、1200ms...)更温和。
  • 重试只针对幂等操作(GET/查询),避免对写操作造成重复副作用。
ts 复制代码
// 见 asyncKit.ts -> withRetry

应用场景举例(2~3 个)

场景一:列表页首屏并发加载(配置 + 资源 + 推荐)

需求 :首屏要同时拉 3 个接口,但又不希望对端被瞬时压垮。
做法:把 3 个任务丢给并发池,限制并发为 2;任一失败也不要阻塞页面,可先部分渲染。

ts 复制代码
import { runWithPool } from './asyncKit';
import { httpGet } from './asyncKit';

async function loadHomeFirstScreen() {
  const tasks = [
    () => httpGet('/api/config'),
    () => httpGet('/api/resources'),
    () => httpGet('/api/recommend'),
  ];
  const results = await runWithPool(tasks, 2); // 并发 2
  const [config, resources, recommend] = results;
  // 容错:失败的部分用降级数据或空态
  return {
    config: config instanceof Error ? {} : config,
    resources: resources instanceof Error ? [] : resources,
    recommend: recommend instanceof Error ? [] : recommend,
  };
}

要点

  • "先出结果的先渲染",把首屏时间做好;失败项做降级。
  • 后续滚动或次要模块慢慢补齐。

场景二:慢接口 + 超时 + 重试 + 兜底缓存

需求 :某接口偶发超时,需要给用户"尽量稳"的体验。
做法:请求超时后进行指数退避重试;重试多次失败,回退到本地缓存。

ts 复制代码
import { withRetry, httpGet } from './asyncKit';

async function fetchUserProfile() {
  try {
    const data = await withRetry(
      () => httpGet('/api/user/profile', 2500),
      3, // 3 次机会
      400 // 初始退避
    );
    // 成功更新本地缓存
    await saveCache('user_profile', data);
    return data;
  } catch {
    // 失败则读取兜底缓存
    const cached = await loadCache('user_profile');
    if (cached) return cached;
    throw new Error('用户信息获取失败且无缓存');
  }
}

// 这里用文件做个简单缓存示意
import { writeTextFile, readTextFile } from './asyncKit';
async function saveCache(key: string, data: any) {
  await writeTextFile(`/data/storage/el2/base/files/${key}.json`, JSON.stringify(data));
}
async function loadCache(key: string) {
  try {
    const txt = await readTextFile(`/data/storage/el2/base/files/${key}.json`);
    return JSON.parse(txt);
  } catch {
    return null;
  }
}

要点

  • 重试要有上限 ,并且幂等
  • 缓存是兜底,不保证新鲜,但能稳住体验。

场景三:搜索输入节流 + 可取消的后端查询

需求 :用户在搜索框频繁输入,我们只想发"最后一次"的请求,之前的要取消。
做法 :输入变化时创建新的 CancelToken,旧的 token 取消;配合小延迟做节流。

ts 复制代码
import { CancelToken, httpGetCancellable, delay } from './asyncKit';

let currentToken: CancelToken | null = null;

export async function searchSmart(q: string) {
  // 简单节流/防抖:等待 200ms 看输入是否稳定
  await delay(200);

  // 取消上一次
  if (currentToken) currentToken.cancel();
  currentToken = new CancelToken();

  try {
    const res = await httpGetCancellable(`/api/search?q=${encodeURIComponent(q)}`, currentToken, 5000);
    return res;
  } catch (e) {
    if ((e as Error).message.includes('Cancelled')) {
      // 被取消视为正常流程,不提示
      return null;
    }
    throw e;
  }
}

要点

  • 搜索请求往往"多、散、无序",取消非常关键。
  • 被取消不是错误,是"正常早停"。

QA 环节

Q1:系统 API 已经有超时参数了,为什么还要 Promise.race 再包一层超时?

A:双保险。不同网络阶段(DNS、TLS、服务端处理)可能表现不一致,Promise.race 能给你"最外层"的可控时限。实测遇到偶发卡死时,这层很有用。

Q2:为什么不用全局 Promise.all

A:Promise.all 会在第一处 reject 直接短路,且同时放飞所有请求;对于"页面多个区块"这类场景,不如"并发池 + allSettled/容错"的策略稳。

Q3:取消请求有没有更"硬"的方式?

A:这里示例用的是"软取消"(业务层检查 token 自行早停)。某些底层能力不支持直接中断连接,或者中断后善后成本高;软取消更安全、可控。若后续 SDK 提供硬取消且你能正确善后,也可以用。

Q4:重试会不会放大问题?

A:会。一定要限制尝试次数,并配合指数退避,最好只对 GET/查询这类幂等操作生效。写操作(POST/PUT/DELETE)要确认幂等性(比如用幂等键)再考虑重试。

Q5:文件路径为什么放在 /data/storage/el2/base/files/

A:这是应用私有可写目录,读写权限最稳。实际项目请结合应用沙箱与权限模型,避免写到不该写的地方。

总结

鸿蒙里的异步处理,并不玄学:async/await 写业务Promise 工具层兜底 ,再配上并发池、超时/取消、重试/退避、缓存兜底,就能把大部分"不可靠的网络与 I/O"变得可预期。本文的 Demo 给了一个能跑的小框架:

  • 页面按钮触发不同策略,观察真实效果;
  • 工具模块沉淀成你项目的"网络与 I/O 中台";
  • 场景案例覆盖首屏并发、慢接口容错、搜索可取消。

你可以直接把 asyncKit.ts 抽到公共库里用,把页面换成你自己的业务 UI。如果需要,我可以帮你把这些工具函数改造成带"请求拦截/响应拦截、自动注入 token、统一错误码处理"的完整网络层模板。

相关推荐
爱笑的眼睛116 小时前
HarmonyOS TextArea 组件:文本输入区域的简单使用指南
华为·harmonyos
祥睿夫子8 小时前
鸿蒙ArkTS开发:Number、Boolean、String三种核心基本数据类型详解(附实战案例)
harmonyos·arkts
小喷友8 小时前
第5章 高级UI与动画
前端·app·harmonyos
whysqwhw8 小时前
鸿蒙ArkTS 与 Native 交互场景分类总结与关键实现
harmonyos
爱笑的眼睛118 小时前
HarmonyOS 递归实战:文件夹文件统计案例解析
华为·harmonyos
鸿蒙小白龙9 小时前
openharmony之启动恢复子系统详解
harmonyos·鸿蒙·鸿蒙系统
GeniuswongAir12 小时前
交叉编译.so到鸿蒙使用
华为·harmonyos
keepDXRcuriosity14 小时前
ArkTS 语言全方位解析:鸿蒙生态开发新选择
华为·harmonyos·arkts·鸿蒙
whysqwhw14 小时前
鸿蒙图标快捷菜单
harmonyos