散修带你入门鸿蒙应用开发基础第十八节:吃透Promise异步并发解决地狱回调

ArkTS基础:第十八节:吃透Promise异步并发解决地狱回调

炼气十八重天

【学习目标】

  1. 理解同步与异步的核心差异,明确ArkTS中异步的适用场景(耗时操作必用)。
  2. 识别回调地狱的弊端,知晓Promise诞生的核心目的。
  3. 掌握Promise的三种状态及不可逆流转规则,能通过resolve/reject控制异步状态。
  4. 熟练使用then/catch/finally处理Promise结果与错误,实现链式调用。
  5. 理解ArkTS异步的本质(单线程+事件循环+微/宏任务调度)。
  6. 会用Promise.all/Promise.race实现异步并发,处理多个独立异步任务。

【学习重点】

  1. Promise的三种状态(pending/fulfilled/rejected)及不可逆流转逻辑。
  2. then/catch/finally的链式调用用法,以及统一错误处理机制。
  3. 微任务与宏任务的优先级差异,事件循环的执行流程。
  4. Promise.allPromise.race的适用场景及代码实操。
  5. 回调地狱的解决思路(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秒的操作),都必须用异步,否则程序会卡死或出现界面卡顿:

  1. 网络请求(调用后端接口、获取远程数据);
  2. 本地文件/数据库读写(读配置文件、存数据到SQLite);
  3. 定时器(延迟执行、定时任务);
  4. 大数据量处理(比如几万条数据的排序、解析)。

三、回调地狱:异步嵌套的坑(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 回调地狱的三个致命问题

  1. 代码可读性差:嵌套层级越多,代码越像“金字塔”,逻辑难以梳理;
  2. 错误处理麻烦:每个回调里都要写错误判断,冗余且容易遗漏;
  3. 维护性差:新增步骤需要在嵌套最深处修改,容易引发bug。

核心诉求:Promise的出现就是为了解决这个问题——把“往右长”的嵌套代码,变成“往下长”的链式代码。

四、Promise:解决回调地狱的核心方案

Promise是ArkTS里处理异步的标准化工具,本质是一个“异步任务状态管理器”,能把回调嵌套转化为链式调用。

4.1 Promise的核心:三种状态与流转规则

Promise只有三种状态,且状态一旦变更,永远不可逆(这是Promise的核心特性):

  1. pending(进行中):异步任务还在执行,初始状态;
  2. fulfilled(成功):异步任务完成,调用resolve(结果)触发状态变更;
  3. rejected(失败):异步任务出错,调用reject(错误)触发状态变更。

流转逻辑

graph TD A[创建Promise] --> B[pending] B --> C[fulfilled] B --> D[rejected] C --> E[then获取结果] D --> F[catch捕获错误] C --> G[finally收尾] D --> G[finally收尾]

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 核心改进:对比回调地狱

  1. 可读性提升:代码从“往右长”变成“往下长”,逻辑一目了然;
  2. 错误处理简化:仅需一个catch,即可捕获所有步骤的错误;
  3. 维护性提升:新增步骤只需添加一个then,无需修改原有嵌套结构。

4.4 易错点提醒(重点)

⚠️ 易错点1:忘记调用resolve/reject → Promise会一直停留在pending状态,结果永远无法处理;
⚠️ 易错点2:认为状态可以修改 → 比如先调用resolve再调用reject,后续调用会直接失效;
⚠️ 易错点3:忘记添加catch → 异步错误会变成“未捕获错误”,导致程序报错崩溃;
⚠️ 易错点4:在Promise执行器里写同步耗时代码(如10秒长循环)→ 会阻塞主线程,失去Promise的异步意义;执行器内应放异步操作(如setTimeout、网络请求)。

五、ArkTS异步的本质:单线程+事件循环

很多新手误以为异步是“多线程同时执行”,其实这是误区!ArkTS继承了JavaScript的单线程特性,异步的本质是任务调度

5.1 核心概念:调用栈 + 任务队列 + 事件循环

我们可以把ArkTS的执行机制想象成一个“餐厅后厨”:

  1. 调用栈:后厨的“操作台”,同步代码依次入栈执行,执行完出栈(一次只能处理一个任务);
  2. 任务队列:后厨的“订单队列”,存放异步任务的回调函数,分为两种类型:
    • 微任务(加急单):优先级高,比如Promise的then/catch/finallyqueueMicrotask
    • 宏任务(普通单):优先级低,比如setTimeoutsetInterval、网络请求、UI事件;
  3. 事件循环:后厨的“调度员”,不停循环执行以下步骤:
    • 执行调用栈里的所有同步代码,直到栈空;
    • 执行微任务队列里的所有回调,直到队列空;
    • 从宏任务队列里取第一个回调执行;
    • 重复上述步骤。

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.allPromise.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> 是合法的,但存在语义偏差

  1. Promise的泛型参数表示resolve的返回类型,而非reject的错误类型;
  2. Promise<Error>会让ArkTS推断Promise.race的返回值为Promise<string | Error>,导致.then()里需要写result: string | Error
  3. 但实际执行中,timeout只有reject,永远不会走.then(),所以result不可能是Error类型。

因此,Promise<never>语义最精准的选择:它明确表示该Promise永远不会有成功结果,ArkTS会自动忽略其对Promise.race返回类型的影响,让.then()里的result只需标注string

  1. nevernullundefined同样都是特殊类型,基础第一节内容我们只讲了后两个。

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)
  }
}

七、内容总结

  1. 同步与异步:同步阻塞主线程(一步等一步,总耗时相加,如两个5秒任务总耗时10秒),异步非阻塞主线程(主线程可做其他事,总耗时为最长任务时间,如5秒异步+5秒同步总耗时5秒);异步的非阻塞特性是处理耗时操作的关键。
  2. Promise核心:三种状态(pending/fulfilled/rejected)不可逆,通过then/catch/finally实现链式调用,统一处理结果与错误,解决回调地狱的问题。
  3. 异步本质:ArkTS是单线程模型,异步依赖事件循环调度,微任务优先级高于宏任务,回调函数需等待主线程空闲后执行。
  4. 异步并发Promise.all等待所有任务完成(适合依赖所有结果的场景,总耗时按最长任务计算),Promise.race取第一个完成的任务(适合超时控制场景);处理超时场景时推荐用Promise<never>标注超时任务,保证类型语义精准。

八、代码仓库

工程名称:PromiseAsyncBasicDemo
本节代码已同步至:https://gitee.com/juhetianxia321/harmony-os-code-base.git

九、下节预告

通过本节学习,我们掌握了Promise的基础语法、异步并发核心(Promise.all/Promise.race)以及异步编程的本质。下一节将在此基础上,进一步简化异步代码写法,深入学习:

  1. async/await语法糖的核心使用(Promise的“同步化”写法,彻底摆脱链式调用冗余);
  2. Promise.allSettled/Promise.any进阶并发方案(覆盖“批量统计成败”“多源容错获取”等实际场景);
  3. Promise开发中的常见坑(await滥用导致并发变串行等问题)及解决方案。

下一节将让异步编程更直观、更高效,为复杂场景的异步处理打下基础~

十、鸿蒙开发者学习与认证指引

(一)、官方学习班级报名(免费)

  1. 班级链接HarmonyOS赋能资源丰富度建设(第四期)
  2. 学号填写规则:填写个人手机号码即可完成班级信息登记

(二)、HarmonyOS应用开发者认证考试(免费)

  1. 考试链接HarmonyOS开发者能力认证入口

  2. 认证等级及适配人群

    • 基础认证:适配软件工程师、移动应用开发人员,需掌握HarmonyOS基础概念、DevEco Studio基础使用、ArkTS及ArkUI基础开发等能力;
    • 高级认证:适配项目经理、工程架构师,需掌握系统核心技术理念、应用架构设计、关键技术开发及应用上架运维等能力;
    • 专家认证:适配研发经理、解决方案专家,需掌握分布式技术原理、端云一体化开发、跨端迁移及性能优化等高级能力。
  3. 认证权益:通过认证可获得电子版证书以及其他专属权益。

posted @ 2025-12-16 18:06  鸿蒙-散修  阅读(5)  评论(0)    收藏  举报