散修带你入门鸿蒙应用开发基础第十八节:吃透Promise异步并发解决地狱回调
ArkTS基础:第十八节:吃透Promise异步并发解决地狱回调
炼气十八重天
【学习目标】
- 理解同步与异步的核心差异,明确ArkTS中异步的适用场景(耗时操作必用)。
- 识别回调地狱的弊端,知晓Promise诞生的核心目的。
- 掌握Promise的三种状态及不可逆流转规则,能通过
resolve/reject控制异步状态。 - 熟练使用
then/catch/finally处理Promise结果与错误,实现链式调用。 - 理解ArkTS异步的本质(单线程+事件循环+微/宏任务调度)。
- 会用
Promise.all/Promise.race实现异步并发,处理多个独立异步任务。
【学习重点】
- Promise的三种状态(pending/fulfilled/rejected)及不可逆流转逻辑。
then/catch/finally的链式调用用法,以及统一错误处理机制。- 微任务与宏任务的优先级差异,事件循环的执行流程。
Promise.all与Promise.race的适用场景及代码实操。- 回调地狱的解决思路(Promise链式调用替代嵌套)。
一、工程准备
1.1 工程结构
本节工程名称为PromiseAsyncBasicDemo,基于鸿蒙5.0(API12)创建,推荐使用DevEco Studio 6.0+开发工具。
为了让代码职责更清晰,我们采用分文件模块化管理,工程结构如下:
PromiseAsyncBasicDemo/
└── ets/pages
├── Index.ets // 入口文件,仅负责调用PromiseTest中的示例函数
└── PromiseTest.ets // 所有Promise相关示例代码的编写与封装
1.2 代码文件分工与调用逻辑
- PromiseTest.ets:将所有示例代码封装为独立的导出函数,便于模块化管理、复用和单独调试。
- Index.ets:作为程序入口,仅负责导入并执行
PromiseTest.ets中的函数,保持入口文件简洁。
二、先搞懂:同步和异步到底是什么?
2.1 用生活例子秒懂同步vs异步
| 类型 | 生活场景(银行办事/日常做事) | 代码中的对应逻辑 |
|---|---|---|
| 同步 | 排队办事,必须等前一个人办完你才能办,期间只能站着等; 吃饭→写作业→睡觉,必须做完一个才能做下一个 |
代码按顺序执行,上一行代码没执行完,下一行绝对不执行,主线程被阻塞 |
| 异步 | 取号后坐着等叫号,取号后可以玩手机,到号了再去办事; 煮开水的同时玩手机,水开了再泡茶 |
代码发起任务后立即返回,主线程继续执行其他代码,任务完成后触发回调函数执行 |
2.2 代码里的同步与异步:阻塞 vs 非阻塞
示例1:同步/异步对比
① 同步模式:一步等一步,阻塞主线程
// 同步示例:吃饭→写作业→睡觉,必须做完一个才能做下一个
export function syncSimpleDemo() {
console.log("===== 【同步模式】开始 =====");
// 1. 吃饭(耗时5秒,模拟同步耗时操作)
const eat = (): void => {
console.log("开始吃饭(预计耗时5秒)");
const start = Date.now();
while (Date.now() - start < 5000) {} // 空循环模拟耗时5秒
console.log("吃饭完成✅");
};
// 2. 写作业(耗时5秒)
const doHomework = (): void => {
console.log("开始写作业(预计耗时5秒)");
const start = Date.now();
while (Date.now() - start < 5000) {}
console.log("写作业完成✅");
};
// 按顺序执行:吃饭→写作业→输出结束(一步等一步)
eat(); // 必须等吃饭完成,才会执行下一行
doHomework(); // 必须等写作业完成,才会执行下一行
console.log("===== 【同步模式】结束(总耗时10秒) =====\n");
}
执行结果:
===== 【同步模式】开始 =====
开始吃饭(预计耗时5秒)
// 等待5秒
吃饭完成✅
开始写作业(预计耗时5秒)
// 等待5秒
写作业完成✅
===== 【同步模式】结束(总耗时10秒) =====
核心结论:同步任务阻塞主线程,总耗时是所有任务时间相加(5+5=10秒),期间程序无法做其他事。
② 异步模式:发起任务后立即返回,非阻塞主线程
// 异步示例:煮开水(异步5秒)→同时玩手机(5秒)→水开了泡茶(直观体现并行)
export function asyncSimpleDemo() {
console.log("===== 【异步模式】开始 =====");
// 1. 煮开水(异步任务:用setTimeout模拟,耗时5秒)
const boilWater = (): void => {
console.log("开始煮开水(发起异步任务,5秒后完成)");
setTimeout(() => {
// 异步回调:水开了才会执行
console.log("水开了✅→开始泡茶");
}, 5000);
};
// 2. 玩手机(同步任务:耗时5秒,和异步任务时长一致)
const playPhone = (): void => {
console.log("开始玩手机(主线程继续做事,预计耗时5秒)");
const start = Date.now();
while (Date.now() - start < 5000) {}
console.log("玩手机完成✅");
};
// 执行顺序:发起煮开水→立即玩手机→输出结束(不用等水开)
boilWater(); // 发起异步任务后,立即返回,不阻塞
playPhone(); // 主线程马上执行,不用等水开
console.log("===== 【异步模式】结束(总耗时5秒) =====\n");
}
执行结果(主线程不等待,总耗时5秒):
===== 【异步模式】开始 =====
开始煮开水(发起异步任务,5秒后完成)
开始玩手机(主线程继续做事,预计耗时5秒)
// 等待5秒(异步任务和同步任务同时完成)
水开了✅→开始泡茶
玩手机完成✅
===== 【异步模式】结束(总耗时5秒) =====
核心结论:异步任务不阻塞主线程,主线程在做5秒的玩手机任务时,异步的煮开水任务在后台并行执行,两者同时完成——总耗时是最长任务的时间(5秒),而非任务时间相加(5+5=10秒),直观体现异步的效率优势。
这种非阻塞的特性,正是ArkTS中处理耗时操作的关键——如果把上述异步任务换成网络请求、文件读写等真实耗时操作,就能避免主线程被阻塞导致的界面卡顿问题。
2.3 异步场景
只要涉及“耗时操作”(尤其是超过1秒的操作),都必须用异步,否则程序会卡死或出现界面卡顿:
- 网络请求(调用后端接口、获取远程数据);
- 本地文件/数据库读写(读配置文件、存数据到SQLite);
- 定时器(延迟执行、定时任务);
- 大数据量处理(比如几万条数据的排序、解析)。
三、回调地狱:异步嵌套的坑(Promise的诞生原因)
异步操作最初用回调函数处理结果,但如果是多个有依赖的异步任务(比如先拿用户ID→再拿用户详情→再格式化详情),回调嵌套会变成“地狱”。
3.1 看代码:依赖型异步任务的嵌套痛点
// PromiseTest.ets - 回调地狱演示
export function callbackHellDemo() {
console.log("===== 回调地狱演示开始 =====");
// 模拟异步获取用户ID(网络请求,延迟1秒)
const getUserId = (callback: (id: string) => void) => {
setTimeout(() => callback("user_1001"), 1000);
};
// 模拟异步获取用户详情(依赖用户ID,延迟1秒)
const getUserDetail = (id: string, callback: (detail: string) => void) => {
setTimeout(() => callback(`ID:${id},姓名:鸿蒙开发者`), 1000);
};
// 模拟异步格式化详情(依赖用户详情,延迟1秒)
const formatDetail = (detail: string, callback: (result: string) => void) => {
setTimeout(() => callback(`【用户信息】${detail}`), 1000);
};
// 执行:3层嵌套形成“回调地狱”(嵌套层级越多,代码越往右偏)
getUserId((id) => {
getUserDetail(id, (detail) => {
formatDetail(detail, (result) => {
console.log("最终结果:", result);
console.log("===== 回调地狱演示结束(总耗时3秒) =====\n");
});
});
});
}
3.2 回调地狱的三个致命问题
- 代码可读性差:嵌套层级越多,代码越像“金字塔”,逻辑难以梳理;
- 错误处理麻烦:每个回调里都要写错误判断,冗余且容易遗漏;
- 维护性差:新增步骤需要在嵌套最深处修改,容易引发bug。
核心诉求:Promise的出现就是为了解决这个问题——把“往右长”的嵌套代码,变成“往下长”的链式代码。
四、Promise:解决回调地狱的核心方案
Promise是ArkTS里处理异步的标准化工具,本质是一个“异步任务状态管理器”,能把回调嵌套转化为链式调用。
4.1 Promise的核心:三种状态与流转规则
Promise只有三种状态,且状态一旦变更,永远不可逆(这是Promise的核心特性):
- pending(进行中):异步任务还在执行,初始状态;
- fulfilled(成功):异步任务完成,调用
resolve(结果)触发状态变更; - rejected(失败):异步任务出错,调用
reject(错误)触发状态变更。
流转逻辑
4.2 代码实操:链式调用替代嵌套
// PromiseTest.ets - Promise解决方案演示
export function promiseSolutionDemo() {
console.log("===== Promise解决方案演示开始 =====");
// 封装:异步获取用户ID(返回Promise对象,延迟1秒)
const getUserIdPromise = (): Promise<string> => {
// Promise构造函数接收一个执行器函数,参数是resolve和reject
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟10%的失败概率(比如网络波动)
const isSuccess = Math.random() > 0.1;
if (isSuccess) {
resolve("user_1001"); // 成功:传递结果数据
} else {
reject(new Error("获取用户ID失败")); // 失败:传递错误信息
}
}, 1000);
});
};
// 封装:异步获取用户详情(依赖ID,返回Promise对象,延迟1秒)
const getUserDetailPromise = (id: string): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = Math.random() > 0.1;
if (isSuccess) {
resolve(`ID:${id},姓名:鸿蒙开发者`);
} else {
reject(new Error("获取用户详情失败"));
}
}, 1000);
});
};
// 封装:异步格式化详情(依赖详情,返回Promise对象,延迟1秒)
const formatDetailPromise = (detail: string): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = Math.random() > 0.1;
if (isSuccess) {
resolve(`【用户信息】${detail}`);
} else {
reject(new Error("格式化详情失败"));
}
}, 1000);
});
};
// 链式调用:替代嵌套(代码从上到下执行,逻辑清晰)
getUserIdPromise()
.then((id) => {
// 第一个任务成功:执行第二个任务,返回新的Promise
return getUserDetailPromise(id);
})
.then((detail) => {
// 第二个任务成功:执行第三个任务,返回新的Promise
return formatDetailPromise(detail);
})
.then((result) => {
// 第三个任务成功:处理最终结果
console.log("最终结果:", result);
})
.catch((error:Error) => {
// 统一捕获:任意一个步骤失败,都会触发这里(无需每个步骤写错误处理)
console.log("错误:", error.message);
})
.finally(() => {
// 无论成功/失败,都会执行(比如清理资源、隐藏加载动画)
console.log("异步任务流程结束(可清理资源)");
console.log("===== Promise解决方案演示结束(总耗时3秒) =====\n");
});
}
4.3 核心改进:对比回调地狱
- 可读性提升:代码从“往右长”变成“往下长”,逻辑一目了然;
- 错误处理简化:仅需一个
catch,即可捕获所有步骤的错误; - 维护性提升:新增步骤只需添加一个
then,无需修改原有嵌套结构。
4.4 易错点提醒(重点)
⚠️ 易错点1:忘记调用resolve/reject → Promise会一直停留在pending状态,结果永远无法处理;
⚠️ 易错点2:认为状态可以修改 → 比如先调用resolve再调用reject,后续调用会直接失效;
⚠️ 易错点3:忘记添加catch → 异步错误会变成“未捕获错误”,导致程序报错崩溃;
⚠️ 易错点4:在Promise执行器里写同步耗时代码(如10秒长循环)→ 会阻塞主线程,失去Promise的异步意义;执行器内应放异步操作(如setTimeout、网络请求)。
五、ArkTS异步的本质:单线程+事件循环
很多新手误以为异步是“多线程同时执行”,其实这是误区!ArkTS继承了JavaScript的单线程特性,异步的本质是任务调度。
5.1 核心概念:调用栈 + 任务队列 + 事件循环
我们可以把ArkTS的执行机制想象成一个“餐厅后厨”:
- 调用栈:后厨的“操作台”,同步代码依次入栈执行,执行完出栈(一次只能处理一个任务);
- 任务队列:后厨的“订单队列”,存放异步任务的回调函数,分为两种类型:
- 微任务(加急单):优先级高,比如Promise的
then/catch/finally、queueMicrotask; - 宏任务(普通单):优先级低,比如
setTimeout、setInterval、网络请求、UI事件;
- 微任务(加急单):优先级高,比如Promise的
- 事件循环:后厨的“调度员”,不停循环执行以下步骤:
- 执行调用栈里的所有同步代码,直到栈空;
- 执行微任务队列里的所有回调,直到队列空;
- 从宏任务队列里取第一个回调执行;
- 重复上述步骤。
5.2 代码验证:微任务比宏任务先执行
// PromiseTest.ets - 事件循环(微任务/宏任务)验证
export function eventLoopDemo() {
console.log("===== 事件循环(微任务/宏任务)验证开始 =====");
console.log("1. 同步代码开始");
// 宏任务:setTimeout(普通单,延迟0秒)
setTimeout(() => {
console.log("4. 宏任务(setTimeout)执行");
}, 0);
// 微任务:Promise.then(加急单)
Promise.resolve().then(() => {
console.log("3. 微任务(Promise.then)执行");
});
console.log("2. 同步代码结束");
console.log("===== 事件循环验证结束 =====\n");
}
执行结果(看顺序:1→2→3→4)
===== 事件循环(微任务/宏任务)验证开始 =====
1. 同步代码开始
2. 同步代码结束
===== 事件循环验证结束 =====
// 同步代码执行完毕,先清空所有微任务
3. 微任务(Promise.then)执行
// 微任务执行完毕,再执行第一个宏任务
4. 宏任务(setTimeout)执行
关键结论
ArkTS的异步不是多线程并行执行,而是通过事件循环机制,把异步任务的回调放到合适的时机执行,从而实现“非阻塞”效果。
六、Promise异步并发:处理多个独立任务
实际开发中,我们经常需要同时执行多个独立的异步任务(比如同时加载商品列表、用户信息、购物车数据),这时候可以用Promise.all或Promise.race实现并发。
6.1 核心方法对比
| 方法 | 作用 | 特点 |
|---|---|---|
Promise.all |
等待所有任务完成 | 所有任务成功才返回结果数组(顺序与传入一致),一个失败则立即返回失败 |
Promise.race |
取第一个完成的任务 | 无论成功/失败,只要有一个任务完成就返回结果,后续任务忽略 |
6.2 代码实操:异步并发演示
// PromiseTest.ets - Promise异步并发演示
export function promiseConcurrentDemo() {
console.log("----- Promise.all 演示(3个任务同时执行,最长5秒) -----");
const task1:Promise<string> = new Promise((resolve) => setTimeout(() => resolve("任务1完成"), 5000)); // 5秒
const task2:Promise<string> = new Promise((resolve) => setTimeout(() => resolve("任务2完成"), 3000)); // 3秒
const task3:Promise<string> = new Promise((resolve) => setTimeout(() => resolve("任务3完成"), 1000)); // 1秒
Promise.all([task1, task2, task3])
.then((results:string[]) => {
// 结果数组顺序与传入的Promise数组顺序一致(即使task3先完成)
console.log("所有任务完成:", results); // 输出:["任务1完成", "任务2完成", "任务3完成"]
console.log("Promise.all 总耗时:5秒(按最长任务时间计算)");
})
.catch((error:Error) => {
console.log("有任务失败:", error.message);
});
console.log("\n----- Promise.race 演示(超时场景) -----");
// 模拟接口请求(8秒返回数据)
const request:Promise<string> = new Promise((resolve) => setTimeout(() => resolve("接口数据返回"), 8000));
// 模拟超时逻辑(5秒超时,比请求快)
// const timeout: Promise<Error> = new Promise((reject) => setTimeout(() => reject(new Error("请求超时")), 5000));
// 注意:这里resolve参数未使用,符合Promise<never>的语义(永远不会resolve)
const timeout: Promise<never> = new Promise((resolve, reject) => {
// 超时逻辑:调用reject传递错误(正确的Promise失败处理方式)
setTimeout(() => reject(new Error("请求超时")), 5000);
});
Promise.race([request, timeout])
.then((result:string) => {
// 仅当request先完成时触发(需将request延迟改为<5000ms)
console.log("成功:", result);
})
.catch((error:Error) => {
// 实际输出:请求超时(5秒先触发,直观看到超时效果)
console.log("失败:", error.message);
});
console.log("===== Promise异步并发演示结束 =====\n");
}
关键语义说明:为什么不用Promise<Error>?
从代码语法上,const timeout: Promise<Error> 是合法的,但存在语义偏差:
- Promise的泛型参数表示resolve的返回类型,而非reject的错误类型;
Promise<Error>会让ArkTS推断Promise.race的返回值为Promise<string | Error>,导致.then()里需要写result: string | Error;- 但实际执行中,
timeout只有reject,永远不会走.then(),所以result不可能是Error类型。
因此,Promise<never>是语义最精准的选择:它明确表示该Promise永远不会有成功结果,ArkTS会自动忽略其对Promise.race返回类型的影响,让.then()里的result只需标注string。
never和null、undefined同样都是特殊类型,基础第一节内容我们只讲了后两个。
6.3 注意点
Promise.all的结果数组顺序严格对应传入的Promise数组顺序,与任务完成顺序无关;总耗时按最长任务时间计算,而非相加;Promise.race只响应第一个完成的任务,后续任务的结果/错误都会被忽略;适合超时控制场景(比如接口请求5秒没响应就提示超时);- 处理
Promise.race的超时场景时,推荐用Promise<never>标注超时任务(语义精准),避免因类型推断导致的.then()参数类型冗余(如string | Error); - 如果需要处理“所有任务无论成败都要获取结果”的场景,可关注下节的
Promise.allSettled方法。
附:入口文件代码(Index.ets)
// Index.ets - 程序入口文件
// 导入PromiseTest中的所有示例函数
import {
syncSimpleDemo,
asyncSimpleDemo,
callbackHellDemo,
promiseSolutionDemo,
eventLoopDemo,
promiseConcurrentDemo
} from './PromiseTest'
@Entry
@Component
struct Index {
aboutToAppear() {
syncSimpleDemo()
asyncSimpleDemo()
callbackHellDemo()
eventLoopDemo()
promiseConcurrentDemo()
}
build() {
Column() {
Text("Promise异步并发基础示例(直观演示同步/异步差异)")
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
七、内容总结
- 同步与异步:同步阻塞主线程(一步等一步,总耗时相加,如两个5秒任务总耗时10秒),异步非阻塞主线程(主线程可做其他事,总耗时为最长任务时间,如5秒异步+5秒同步总耗时5秒);异步的非阻塞特性是处理耗时操作的关键。
- Promise核心:三种状态(pending/fulfilled/rejected)不可逆,通过
then/catch/finally实现链式调用,统一处理结果与错误,解决回调地狱的问题。 - 异步本质:ArkTS是单线程模型,异步依赖事件循环调度,微任务优先级高于宏任务,回调函数需等待主线程空闲后执行。
- 异步并发:
Promise.all等待所有任务完成(适合依赖所有结果的场景,总耗时按最长任务计算),Promise.race取第一个完成的任务(适合超时控制场景);处理超时场景时推荐用Promise<never>标注超时任务,保证类型语义精准。
八、代码仓库
工程名称:PromiseAsyncBasicDemo
本节代码已同步至:https://gitee.com/juhetianxia321/harmony-os-code-base.git
九、下节预告
通过本节学习,我们掌握了Promise的基础语法、异步并发核心(Promise.all/Promise.race)以及异步编程的本质。下一节将在此基础上,进一步简化异步代码写法,深入学习:
async/await语法糖的核心使用(Promise的“同步化”写法,彻底摆脱链式调用冗余);Promise.allSettled/Promise.any进阶并发方案(覆盖“批量统计成败”“多源容错获取”等实际场景);- Promise开发中的常见坑(
await滥用导致并发变串行等问题)及解决方案。
下一节将让异步编程更直观、更高效,为复杂场景的异步处理打下基础~
十、鸿蒙开发者学习与认证指引
(一)、官方学习班级报名(免费)
- 班级链接:HarmonyOS赋能资源丰富度建设(第四期)
- 学号填写规则:填写个人手机号码即可完成班级信息登记
(二)、HarmonyOS应用开发者认证考试(免费)
-
考试链接:HarmonyOS开发者能力认证入口
-
认证等级及适配人群
- 基础认证:适配软件工程师、移动应用开发人员,需掌握HarmonyOS基础概念、DevEco Studio基础使用、ArkTS及ArkUI基础开发等能力;
- 高级认证:适配项目经理、工程架构师,需掌握系统核心技术理念、应用架构设计、关键技术开发及应用上架运维等能力;
- 专家认证:适配研发经理、解决方案专家,需掌握分布式技术原理、端云一体化开发、跨端迁移及性能优化等高级能力。
-
认证权益:通过认证可获得电子版证书以及其他专属权益。
浙公网安备 33010602011771号