零基础鸿蒙应用开发第三十节:从同步阻塞到异步Promise并发
【学习目标】
- 理解同步与异步的核心差异,明确异步的适用场景(耗时操作必用)。
- 识别回调地狱的弊端,知晓Promise诞生的核心目的。
- 掌握Promise的三种状态及不可逆流转规则,能通过
resolve/reject控制异步状态。 - 熟练使用
then/catch/finally处理Promise结果与错误,实现链式调用。 - 理解异步的本质(单线程+事件循环+微/宏任务调度),明确当前学习的是异步并发而非多线程。
- 会用
Promise.all/Promise.race实现异步并发,处理多个独立异步任务。
【学习重点】
- Promise的三种状态(pending/fulfilled/rejected)及不可逆流转逻辑。
then/catch/finally的链式调用用法。- 微任务与宏任务的优先级差异,事件循环的执行流程。
Promise.all与Promise.race的适用场景及代码实操。- 异步并发(单线程)与多线程并发的核心区别,多线程工具的初步认知。
- 回调地狱的解决思路(Promise链式调用替代嵌套)。
一、工程准备
本节工程名称为PromiseAsyncBasicDemo,基于 鸿蒙5.0(API12) 创建,推荐使用 DevEco Studio 6.0+ 开发工具。
PromiseAsyncBasicDemo/
└── ets/
├── pages/
│ └── Index.ets // 核心页面:5个按钮+点击事件,手动触发示例
└── utils/ // 工具目录(手动创建)
└── PromiseUtil.ets // Promise核心逻辑:所有示例封装为静态方法
二、先搞懂:同步和异步到底是什么?
2.1 核心概念:并发的两种实现方式
并发指在同一时间内多个任务的执行机制,主要分为两种类型:
- 异步并发:异步代码执行到一定阶段后暂停,未来某个时间点继续执行,同一时间只有一段代码执行(单线程模型)。适用于单次I/O任务(网络请求、文件读写、定时器等)、轻量无CPU阻塞的任务、逻辑依赖清晰的任务。
- 多线程并发:允许同时执行多段代码,UI主线程响应用户操作,后台线程执行耗时任务。适用于CPU密集型、长时任务、常驻任务等场景,后续章节将通过TaskPool和Worker工具详细讲解。
重要说明:本节及下一节学习的是异步并发,基于单线程模型,依赖事件循环调度任务;而非多线程并发,多线程涉及线程创建、数据通信等复杂逻辑,将在后续专门章节深入讲解。
2.2 用生活例子秒懂同步vs异步
| 类型 | 生活场景(银行办事/日常做事) | 代码中的对应逻辑 |
|---|---|---|
| 同步 | 排队办事,必须等前一个人办完你才能办,期间只能站着等; 吃饭→写作业→睡觉,必须做完一个才能做下一个 |
代码按顺序执行,上一行代码没执行完,下一行绝对不执行,主线程被阻塞 |
| 异步 | 取号后坐着等叫号,取号后可以玩手机,到号了再去办事; 煮开水的同时玩手机,水开了再泡茶 |
代码发起任务后立即返回,主线程继续执行其他代码,任务完成后触发回调函数执行(单线程内调度,非多线程并行) |
2.3 同步与异步:阻塞 vs 非阻塞
示例1:同步/异步对比
① 同步模式:一步等一步,阻塞主线程
// ets/utils/PromiseUtil.ets
export class PromiseUtil {
/**
* 同步示例:吃饭→写作业→睡觉,必须做完一个才能做下一个
*/
static syncSimpleDemo(): void {
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秒)→水开了泡茶(直观体现并行)
*/
static asyncSimpleDemo(): void {
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秒
写作业完成✅
===== 【同步模式】结束(总耗时10秒) =====
执行结果(异步):
===== 【异步模式】开始 =====
开始煮开水(发起异步任务,5秒后完成)
开始玩手机(主线程继续做事,预计耗时5秒)
// 等待5秒(异步任务和同步任务同时完成)
水开了✅→开始泡茶
玩手机完成✅
===== 【异步模式】结束(总耗时5秒) =====
核心结论:同步任务阻塞主线程,总耗时是所有任务时间相加(5+5=10秒),期间程序无法做其他事;异步任务不阻塞主线程(单线程内调度),总耗时是最长任务的时间(5秒),通过事件循环实现“伪并行”,直观体现异步的效率优势。
2.4 异步适用场景
只要涉及“耗时操作”(尤其是超过1秒的操作),都必须用异步,否则程序会卡死或出现界面卡顿:
- I/O非阻塞操作:网络请求(调用后端接口、获取远程数据)、本地文件/数据库读写(读配置文件、存数据到SQLite);
- 定时器操作:延迟执行、定时任务(setTimeout/setInterval);
- 轻量无CPU阻塞任务:单次执行时间短、无长时间循环的操作;
- 逻辑依赖清晰的任务:有明确顺序或并行关系的异步操作。
三、回调地狱:异步嵌套的坑(Promise的诞生原因)
3.1 看代码:依赖型异步任务的嵌套痛点
// ets/utils/PromiseUtil.ets(继续补充静态方法)
export class PromiseUtil {
// (已包含syncSimpleDemo、asyncSimpleDemo,此处补充)
/**
* 回调地狱演示
*/
static callbackHellDemo(): void {
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:解决回调地狱的核心方案
4.1 Promise的核心定位
Promise是一种用于处理异步操作的对象,核心价值在于将异步操作转换为类似同步操作的风格,通过状态机制管理异步流程,避免回调嵌套,同时提供统一的结果处理和错误捕获接口。
4.2 Promise的核心:三种状态与流转规则
Promise只有三种状态,且状态一旦变更,永远不可逆:
- pending(进行中):异步任务还在执行,初始状态;
- fulfilled(已完成,又称resolved):异步任务完成,调用
resolve(结果)触发状态变更; - rejected(已拒绝):异步任务出错,调用
reject(错误)触发状态变更。
流转逻辑
4.3 代码实操:链式调用替代嵌套
// ets/utils/PromiseUtil.ets(继续补充静态方法)
export class PromiseUtil {
// (已包含syncSimpleDemo、asyncSimpleDemo、callbackHellDemo,此处补充)
/**
* Promise链式调用解决回调地狱
*/
static promiseSolutionDemo(): void {
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.5 易错点提醒(重点)
⚠️ 易错点1:忘记调用resolve/reject → Promise会一直停留在pending状态,结果永远无法处理;
⚠️ 易错点2:认为状态可以修改 → 比如先调用resolve再调用reject,后续调用会直接失效;
⚠️ 易错点3:忘记添加catch → 异步错误会变成“未捕获错误”,导致程序报错崩溃;
⚠️ 易错点4:在Promise执行器里写同步耗时代码(如10秒长循环)→ 会阻塞主线程,失去异步意义;执行器内应放异步操作(如setTimeout、网络请求)。
五、异步的本质:单线程+事件循环
5.1 核心概念:调用栈 + 任务队列 + 事件循环
异步的实现依赖单线程模型下的事件循环机制,可类比为“餐厅后厨”的工作流程:
- 调用栈:相当于后厨的“操作台”,同步代码依次入栈执行,执行完出栈(一次只能处理一个任务);
- 任务队列:相当于后厨的“订单队列”,存放异步任务的回调函数,分为两种类型:
- 微任务:优先级高,比如Promise的
then/catch/finally、queueMicrotask; - 宏任务:优先级低,比如
setTimeout、setInterval、网络请求、UI事件;
- 微任务:优先级高,比如Promise的
- 事件循环:相当于后厨的“调度员”,不停循环执行以下步骤:
- 执行调用栈里的所有同步代码,直到栈空;
- 执行微任务队列里的所有回调,直到队列空;
- 从宏任务队列里取第一个回调执行;
- 重复上述步骤。
5.2 代码验证:微任务比宏任务先执行
// ets/utils/PromiseUtil.ets(继续补充静态方法)
export class PromiseUtil {
// (已包含上述方法,此处补充)
/**
* 事件循环(微/宏任务)验证
*/
static eventLoopDemo(): void {
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)执行
关键结论
异步不是多线程并行执行,而是通过事件循环机制,在单线程内调度异步任务的回调执行,从而实现“非阻塞”效果。真正的多线程需要通过专门工具实现,当前阶段仅需理解单线程下的异步调度逻辑。
六、Promise异步并发:处理多个独立任务
6.1 核心方法对比
| 方法 | 作用 | 特点 |
|---|---|---|
Promise.all |
等待所有任务完成 | 所有任务成功才返回结果数组(顺序与传入一致),一个失败则立即返回失败 |
Promise.race |
取第一个完成的任务 | 无论成功/失败,只要有一个任务完成就返回结果,后续任务忽略 |
6.2 代码实操:Promise.all/Promise.race
// ets/utils/PromiseUtil.ets(继续补充静态方法)
export class PromiseUtil {
// (已包含上述方法,此处补充)
/**
* Promise.all/race 异步并发演示
*/
static promiseConcurrentDemo(): void {
console.log("===== Promise并发演示开始 =====");
// 模拟:异步获取商品列表(延迟2秒)
const getGoodsList = (): Promise<string[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(["商品1", "商品2", "商品3"]);
}, 2000);
});
};
// 模拟:异步获取用户购物车(延迟1.5秒)
const getCart = (): Promise<string[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(["购物车商品1", "购物车商品2"]);
}, 1500);
});
};
// 模拟:超时控制任务(延迟3秒,仅失败)
const timeoutTask = (): Promise<never> => {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("请求超时(3秒未响应)"));
}, 3000);
});
};
// 1. Promise.all:等待所有任务完成(总耗时2秒,取最长任务时间)
Promise.all([getGoodsList(), getCart()])
.then((result: Array<string[]>) => {
const goods = result[0];
const cart = result[1];
console.log("Promise.all 成功结果:");
console.log("商品列表:", goods);
console.log("购物车:", cart);
})
// 2. Promise.race:取第一个完成的任务(1.5秒后返回购物车结果)
Promise.race([getGoodsList(), getCart(), timeoutTask()])
.then((result) => {
console.log("Promise.race 第一个完成的结果:", result);
})
.catch((error:Error) => {
console.log("Promise.race 失败:", error.message);
});
console.log("===== Promise并发演示结束(代码已发起,等待异步结果) =====\n");
}
}
6.3 关键语义说明:为什么不用Promise<Error>?
从代码语法上,const timeout: Promise<Error> 是合法的,但存在语义偏差:
- Promise的泛型参数表示resolve的返回类型,而非reject的错误类型;
Promise<Error>会导致.then()里需要写result: string | Error,但实际timeout永远不会走.then(),result不可能是Error类型;Promise<never>是语义最精准的选择:明确表示该Promise永远不会有成功结果,不会影响Promise.race的返回类型推断,让.then()里的result只需标注string。
6.4 注意点
Promise.all的结果数组顺序严格对应传入的Promise数组顺序,与任务完成顺序无关;总耗时按最长任务时间计算,而非相加;Promise.race只响应第一个完成的任务,后续任务的结果/错误都会被忽略;适合超时控制场景(比如接口请求5秒没响应就提示超时);- 处理
Promise.race的超时场景时,推荐用Promise<never>标注超时任务(语义精准),避免类型推断冗余; - 如果需要处理“所有任务无论成败都要获取结果”的场景,可关注下节的
Promise.allSettled方法。
附:入口文件代码(Index.ets)
// ets/pages/Index.ets - 程序入口文件
import { PromiseUtil } from '../utils/PromiseUtil';
@Entry
@Component
struct Index {
build() {
Column({ space: 15 }) {
Text("Promise异步并发基础示例(直观演示同步/异步差异)")
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin(20);
// 1. 同步/异步对比按钮
Button("1. 同步 vs 异步 对比")
.width('80%')
.height(50)
.onClick(() => {
PromiseUtil.syncSimpleDemo();
PromiseUtil.asyncSimpleDemo();
});
// 2. 回调地狱演示按钮
Button("2. 回调地狱演示")
.width('80%')
.height(50)
.onClick(() => PromiseUtil.callbackHellDemo());
// 3. Promise 链式调用按钮
Button("3. Promise 链式调用")
.width('80%')
.height(50)
.onClick(() => PromiseUtil.promiseSolutionDemo());
// 4. 微/宏任务 验证按钮
Button("4. 微/宏任务 验证")
.width('80%')
.height(50)
.onClick(() => PromiseUtil.eventLoopDemo());
// 5. 异步并发演示按钮
Button("5. Promise.all/race 并发")
.width('80%')
.height(50)
.onClick(() => PromiseUtil.promiseConcurrentDemo());
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
}
七、内容总结
- 同步与异步:同步阻塞主线程(总耗时相加),异步非阻塞主线程(总耗时为最长任务时间),耗时操作必须用异步避免界面卡顿;
- Promise核心:三种状态(pending/fulfilled/rejected)不可逆,通过
then/catch/finally实现链式调用,统一处理结果与错误,解决回调地狱; - 异步本质:基于单线程模型,依赖事件循环调度,微任务优先级高于宏任务,非多线程并行;
- 异步并发:
Promise.all等待所有任务完成(适合依赖所有结果),Promise.race取第一个完成的任务(适合超时控制);超时场景推荐用Promise<never>标注超时任务,保证类型语义精准; - 多线程说明:多线程并发适用于CPU密集型、长时任务,需通过TaskPool和Worker工具实现,后续章节将详细讲解。
八、代码仓库
- 工程名称:
PromiseAsyncBasicDemo - 仓库地址:https://gitee.com/juhetianxia321/harmony-os-code-base.git
九、下节预告
通过本节的学习,我们已经掌握了 Promise 的基础语法、then/catch/finally链式调用,以及Promise.all/Promise.race的基础并发方案。但在复杂业务场景中,链式调用仍有冗余,且错误处理不够直观,同时还有批量任务统计、多源容错等场景未覆盖。下一节将解决这些问题,系统学习:
try/catch/finally错误处理基础:掌握同步/异步场景的错误捕获逻辑,为async/await铺垫;async/await语法糖的核心使用:将Promise异步代码“同步化”书写,彻底摆脱链式调用的冗余;Promise进阶并发方案:Promise.allSettled/Promise.any的使用场景与实操,覆盖批量任务统计、多源数据容错获取等场景;Promise开发避坑:解决await滥用导致的并发变串行问题,优化异步任务执行性能。
下一节将让异步编程更直观、更高效,同时完善错误处理和并发逻辑,为复杂场景的异步开发打下坚实基础~
浙公网安备 33010602011771号