云计算百科
云计算领域专业知识百科平台

【HarmonyOS开发小实践】异步并发Promise 和 async/await 怎么用

异步并发Promise 和 async/await 怎么用

网络请求、文件读写、定时器,这些操作有个共同点:发起之后不能立刻拿到结果,得等。等的时候要是把主线程卡住,UI 就冻住不动了。所以这些操作得异步——发起后先让主线程干别的,结果回来再处理。ArkTS 处理异步的主要手段就是 Promise 和 async/await,这俩是标准 JS 异步语法,在 ArkTS 里用法基本一致。这篇把异步并发的概念、Promise 的状态机制、async/await 的用法,以及错误处理讲透,配上能跑的代码。

异步并发是什么

异步并发是一种编程语言的特性,允许程序在执行某些操作时不必等待其完成,可以继续执行其他异步代码。异步代码执行时会被挂起,在异步操作完成后恢复执行,确保同一时间只有一段代码在运行。

注意这里的关键点:“同一时间只有一段代码在运行”。异步并发不是多线程,不是真的同时跑两段代码。它是单线程下的调度——遇到要等的操作就挂起当前代码,去执行别的代码,等操作完成了再回来继续。

典型的异步并发使用场景:

  • I/O 非阻塞操作:网络请求、文件读写、定时器等
  • 任务轻量且无 CPU 阻塞:单次任务执行时间短
  • 逻辑依赖清晰:任务有明确的顺序或并行关系

如果是 CPU 密集的耗时计算,异步并发帮不了你——计算时 CPU 在转,没法让出去。那种得用多线程,是后面文章讲的内容。

Promise:异步操作的对象化

Promise 是一种用于处理异步操作的对象,把异步操作包装成对象,可以像同步操作那样写代码。它通过状态机制管理异步操作的不同阶段。

Promise 的三种状态

Promise 有三种状态:

  • pending:进行中。刚创建时的状态
  • fulfilled:已完成(也叫 resolved)。异步操作成功
  • rejected:已拒绝。异步操作失败

状态转换是单向不可逆的:pending 可以变成 fulfilled 或 rejected,但变了就变不回来,也不会从 fulfilled 变成 rejected。

#mermaid-svg-2vEbeMaCL3dPWZKD{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2vEbeMaCL3dPWZKD .error-icon{fill:#552222;}#mermaid-svg-2vEbeMaCL3dPWZKD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2vEbeMaCL3dPWZKD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2vEbeMaCL3dPWZKD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2vEbeMaCL3dPWZKD .marker.cross{stroke:#333333;}#mermaid-svg-2vEbeMaCL3dPWZKD svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2vEbeMaCL3dPWZKD p{margin:0;}#mermaid-svg-2vEbeMaCL3dPWZKD defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-2vEbeMaCL3dPWZKD g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-2vEbeMaCL3dPWZKD g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-2vEbeMaCL3dPWZKD g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-2vEbeMaCL3dPWZKD g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-2vEbeMaCL3dPWZKD g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-2vEbeMaCL3dPWZKD .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-2vEbeMaCL3dPWZKD .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-2vEbeMaCL3dPWZKD .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-2vEbeMaCL3dPWZKD .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2vEbeMaCL3dPWZKD .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-2vEbeMaCL3dPWZKD .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-2vEbeMaCL3dPWZKD .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-2vEbeMaCL3dPWZKD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2vEbeMaCL3dPWZKD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2vEbeMaCL3dPWZKD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2vEbeMaCL3dPWZKD .edgeLabel .label text{fill:#333;}#mermaid-svg-2vEbeMaCL3dPWZKD .label div .edgeLabel{color:#333;}#mermaid-svg-2vEbeMaCL3dPWZKD .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-2vEbeMaCL3dPWZKD .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-2vEbeMaCL3dPWZKD .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-2vEbeMaCL3dPWZKD .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-2vEbeMaCL3dPWZKD .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-2vEbeMaCL3dPWZKD .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2vEbeMaCL3dPWZKD .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2vEbeMaCL3dPWZKD #statediagram-barbEnd{fill:#333333;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2vEbeMaCL3dPWZKD .cluster-label,#mermaid-svg-2vEbeMaCL3dPWZKD .nodeLabel{color:#131300;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-2vEbeMaCL3dPWZKD .note-edge{stroke-dasharray:5;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-note text{fill:black;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram-note .nodeLabel{color:black;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagram .edgeLabel{color:red;}#mermaid-svg-2vEbeMaCL3dPWZKD #dependencyStart,#mermaid-svg-2vEbeMaCL3dPWZKD #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-2vEbeMaCL3dPWZKD .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2vEbeMaCL3dPWZKD :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

创建 Promise

resolve() 调用

reject() 调用

pending

fulfilled

rejected

进行中,等待异步操作完成

操作成功,结果可用

操作失败,错误可用

创建 Promise

最基本的用法是通过构造函数实例化一个 Promise 对象,传入一个带有两个参数的函数,称为 executor 函数。executor 函数接收两个参数:resolve 和 reject,分别表示异步操作成功和失败时的回调函数。

// 创建一个 Promise 模拟异步操作
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
const randomNumber: number = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber); // 成功,把结果传出去
} else {
reject(new Error('Random number is too small')); // 失败,把错误传出去
}
}, 1000);
});

这段代码里,setTimeout 模拟一个 1 秒后完成的异步操作。1 秒后生成随机数,大于 0.5 就 resolve(成功),否则 reject(失败)。创建后 Promise 处于 pending 状态,1 秒后状态才确定。

then / catch / finally

Promise 提供了 then、catch、finally 方法来注册回调函数,处理异步操作的成功或失败结果。当 Promise 状态改变时,回调函数会被加入微任务队列等待执行,依赖事件循环机制在宏任务执行完成后优先执行微任务,从而保证回调函数的异步调度。

import { BusinessError } from '@kit.BasicServicesKit';

// then 接收两个参数:成功回调和失败回调
promise.then((result: number) => {
console.info(`成功,结果是 ${result}`);
}, (error: BusinessError) => {
console.error(`失败,code: ${error.code}, message: ${error.message}`);
});

// 更常见的写法:then 处理成功,catch 处理失败
promise.then((result: number) => {
console.info(`成功,结果是 ${result}`);
}).catch((error: BusinessError) => {
console.error(`失败,code: ${error.code}, message: ${error.message}`);
});

// finally 不管成功失败都执行
promise.finally(() => {
console.info('操作结束,无论成功失败');
});

then 的链式调用是 Promise 的核心能力。前一个 then 的返回值会传给后一个 then:

// 链式调用,每一步的返回传给下一步
fetchUserId()
.then((userId: number) => fetchUserDetail(userId)) // 用 id 查详情
.then((detail: UserDetail) => fetchUserAvatar(detail.avatarId)) // 用详情查头像
.then((avatar: string) => {
console.info(`头像 URL: ${avatar}`);
})
.catch((error: BusinessError) => {
// 任何一步失败都会到这里
console.error(`链路中某步失败: ${error.message}`);
});

链式调用的好处是把多步异步操作写成线性的,不用层层嵌套回调(回调地狱)。每一步的输出是下一步的输入,读起来像同步代码。

未处理的 reject

当 Promise 被 reject 且未通过 catch 方法处理时,会触发 globalUnhandledRejectionDetected 事件。可以用 errorManager.on('globalUnhandledRejectionDetected') 接口监听这个事件,全局捕获未处理的 Promise reject。

import { errorManager } from '@kit.BasicServicesKit';

// 全局监听未处理的 Promise reject
errorManager.on('globalUnhandledRejectionDetected', (e) => {
console.error(`有 Promise reject 没被处理: ${e.reason}`);
});

这个监听在应用里建议加上。Promise 链里漏写 catch 是常见 bug,有了全局监听至少能在日志里看到,不至于静默失败。

async/await:Promise 的语法糖

async/await 是用于处理异步操作的 Promise 语法糖,使编写异步代码更加简单和易读。使用 async 关键字声明异步函数,并使用 await 关键字等待 Promise 的解析(fulfilled 或 rejected),以同步方式编写异步操作的代码。

基本用法

// async 函数返回 Promise
async function myAsyncFunction(): Promise<string> {
// await 等待 Promise 解析
const result: string = await new Promise((resolve: Function) => {
setTimeout(() => {
resolve('Hello, world!');
}, 3000);
});
console.info(result); // 输出:Hello, world!
return result;
}

// 调用 async 函数
@Entry
@Component
struct MyPage {
@State message: string = '等待中';

build() {
Column() {
Text(this.message)
.onClick(async () => {
let res: string = await myAsyncFunction();
this.message = res;
})
}
}
}

async 函数返回 Promise 对象。函数内部可包含零个或多个 await 关键字,await 会暂停执行,直到关联的 Promise 完成状态转换(fulfilled 或 rejected)。若函数执行过程中抛出异常,该异常将直接触发返回的 Promise 进入 rejected 状态。

await 只能在 async 函数内用

await 关键字只在 async 函数内有效。这是硬限制,在普通函数里写 await 会报语法错误。

// 这样会报错
// function normalFunc() {
// let result = await someAsyncOp(); // 语法错误
// }

// 必须在 async 函数里
async function asyncFunc(): Promise<void> {
let result = await someAsyncOp(); // 合法
}

实际写代码时,经常遇到"我想在某个回调里 await,但回调不是 async"的情况。解决办法是把回调改成 async:

// 比如按钮点击事件
Button('点击')
.onClick(async () => { // 回调改成 async
let data = await fetchData();
console.info(data);
});

同步代码 vs 异步代码对比

看同一个任务用同步和异步两种写法的区别。假设有三个网络请求要顺序执行:

同步写法(伪代码,实际不存在同步网络请求):

// 同步写法:三步顺序执行,每步都等
let data1 = requestSync('/api/step1'); // 等 1 秒
let data2 = requestSync('/api/step2', data1); // 等 1 秒
let data3 = requestSync('/api/step3', data2); // 等 1 秒
console.info(data3);
// 总耗时 3 秒,期间主线程卡死

Promise 链式写法:

// Promise 链:三步顺序,但每步等待时主线程能干别的
request('/api/step1')
.then((data1) => request('/api/step2', data1))
.then((data2) => request('/api/step3', data2))
.then((data3) => {
console.info(data3);
});
// 总耗时还是 3 秒(顺序依赖),但期间主线程不卡

async/await 写法:

// async/await:看起来像同步,实际是异步
async function fetchAllData(): Promise<void> {
let data1 = await request('/api/step1'); // 等,但主线程能干别的
let data2 = await request('/api/step2', data1);
let data3 = await request('/api/step3', data2);
console.info(data3);
}
// 总耗时 3 秒,主线程不卡,代码读起来像同步

三种写法里,async/await 的可读性最好——代码结构跟同步一样,但没有阻塞主线程。这就是 async/await 的价值:异步的执行,同步的写法。

并行执行多个异步操作

上面是顺序执行。如果三个请求互不依赖,可以并行,用 Promise.all:

async function fetchParallel(): Promise<void> {
// 三个请求同时发起,等最慢的那个完成
let [data1, data2, data3] = await Promise.all([
request('/api/step1'),
request('/api/step2'),
request('/api/step3')
]);
console.info(data1, data2, data3);
}
// 总耗时约 1 秒(三个并行,取最慢的)

注意这里用了解构赋值的写法示意,实际 ArkTS 里要单独取每个结果。并行执行能显著缩短总耗时,前提是操作之间没有依赖。

错误处理

异步代码的错误处理用 try/catch,跟同步代码一样:

async function myAsyncFunction(): Promise<void> {
try {
const result: string = await new Promise((resolve: Function) => {
resolve('Hello, world!');
});
console.info(result);
} catch (e) {
console.error(`Get exception: ${e}`);
}
}

await 后面的 Promise 如果 reject,会抛出异常,被 catch 捕获。这比 Promise 链的 catch 写法更直观。

几种常见的错误处理模式:

// 模式1:整个 async 函数包一个 try/catch
async function mode1(): Promise<void> {
try {
let data1 = await fetchStep1();
let data2 = await fetchStep2(data1);
let data3 = await fetchStep3(data2);
console.info(data3);
} catch (e) {
// 任何一步失败都到这里
console.error(`某步失败: ${e}`);
}
}

// 模式2:每步单独 catch,失败给默认值继续
async function mode2(): Promise<void> {
let data1: string = '';
try {
data1 = await fetchStep1();
} catch (e) {
data1 = '默认值'; // 失败用默认值,继续往下走
}
let data2 = await fetchStep2(data1);
console.info(data2);
}

// 模式3:不 catch,让错误冒泡给调用方
async function mode3(): Promise<string> {
let data = await fetchStep1(); // 失败就抛给调用方
return data;
}
// 调用方处理
mode3().catch((e) => {
console.error(`mode3 失败: ${e}`);
});

选哪种看场景:整条链路任一步失败就全失败用模式1;某步失败不影响后续用模式2;被调函数不管错误让调用方决定用模式3。

举个栗子:带缓存的异步数据加载

把前面讲的串起来看一个完整案例。场景:从缓存读数据,缓存没有就请求网络,请求成功写入缓存。涉及多个异步操作和错误处理。

import { BusinessError } from '@kit.BasicServicesKit';

// 模拟缓存读写
class Cache {
private store: Map<string, string> = new Map();

async get(key: string): Promise<string | null> {
// 模拟异步读缓存
return new Promise((resolve: Function) => {
setTimeout(() => {
resolve(this.store.get(key) ?? null);
}, 50);
});
}

async set(key: string, value: string): Promise<void> {
return new Promise((resolve: Function) => {
setTimeout(() => {
this.store.set(key, value);
resolve();
}, 50);
});
}
}

// 模拟网络请求
async function fetchFromNetwork(url: string): Promise<string> {
return new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
if (url.includes('error')) {
reject(new Error(`请求失败: ${url}`));
} else {
resolve(`数据来自 ${url}`);
}
}, 1000);
});
}

// 带缓存的数据加载
async function loadData(url: string): Promise<string> {
let cache = new Cache();

// 先查缓存
let cached = await cache.get(url);
if (cached !== null) {
console.info('命中缓存');
return cached;
}

// 缓存没有,请求网络
console.info('缓存未命中,请求网络');
let data: string;
try {
data = await fetchFromNetwork(url);
} catch (e) {
let error = e as BusinessError;
console.error(`网络请求失败: ${error.message}`);
throw error; // 往上抛,让调用方知道
}

// 写入缓存(失败不影响返回)
try {
await cache.set(url, data);
console.info('已写入缓存');
} catch (e) {
console.error(`写缓存失败: ${e}`); // 吃掉,不影响主流程
}

return data;
}

// 使用
@Entry
@Component
struct DataPage {
@State content: string = '加载中…';
@State loading: boolean = false;

async loadDataToUI() {
this.loading = true;
try {
let data = await loadData('/api/user/profile');
this.content = data;
} catch (e) {
this.content = '加载失败';
} finally {
this.loading = false;
}
}

build() {
Column() {
Text(this.content)
.fontSize(16)
Button('刷新')
.onClick(() => {
this.loadDataToUI();
})
.enabled(!this.loading)
}
}
}

这段代码里用到了:Promise 构造函数、async/await、try/catch/finally、Promise 的链式语义(await 等待)。错误处理分两层:网络请求失败往上抛,写缓存失败吃掉。finally 保证 loading 状态一定复位。

几条小经验

await 别忘了。 async 函数里调另一个 async 函数,忘了 await 会拿到 Promise 对象而不是结果,后续用结果就出错。这种 bug 比较隐蔽,因为不报错,只是行为不对。代码审查时重点看 await 有没有漏。

Promise.all 要注意短路。 Promise.all 里任一个 Promise reject,整个就 reject。如果想所有都完成不管成功失败,用 Promise.allSettled。想第一个完成就用 Promise.race。根据场景选对方法。

别在循环里串行 await。 如果多个异步操作互不依赖,别写成 for (let item of items) { await process(item); },这是串行执行,慢。用 Promise.all(items.map(item => process(item))) 并行。

finally 里别抛异常。 finally 块里抛异常会覆盖前面的成功结果。finally 只做清理(复位状态、关闭资源),别做可能失败的操作。

回调改成 async 要小心 this。 把普通回调改成 async 回调时,注意 this 指向。箭头函数的 this 是外层作用域的,普通函数的 this 是调用的。在组件里用箭头函数能正确拿到组件 this。

微任务优先级。 Promise 的 then 回调走微任务队列,在宏任务之前执行。理解这个对排查"为什么我的回调比预期早/晚执行"有帮助。事件循环里微任务会清空完才执行下一个宏任务。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【HarmonyOS开发小实践】异步并发Promise 和 async/await 怎么用
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!