HarmonyOS Next面试题之异步并发Promise和async/await的核心机制

一、HarmonyOS Next 中的异步并发

① 异步编程

  • 异步编程的核心思想是"非阻塞",当一个耗时操作(如网络请求)执行时,它不会阻塞主线程,而是先让主线程继续执行后面的代码。待耗时操作完成,再通过回调来通知主线程。
  • 在 UI 应用中,如果所有任务都是同步的,一个耗时的网络请求就会导致应用界面"卡死",产生 ANR(即主线程无法响应用户交互超过 5 秒)。异步编程保证了 UI 的流畅度和应用的即时响应性。

② 异步并发的实现手段和适用场景

  • 主要有异步回调(Promise, async/await)和多线程并发(TaskPool, Worker)两类。
  • 异步 I/O 任务 (Async/Await):适用于 I/O 密集型任务,如网络请求、文件读写等,这些任务的特点是主要时间花在等待外部返回,而非占用 CPU 进行计算,适合挂起主线程进行等待。
  • 多线程并发 (TaskPool/Worker):适用于 CPU 密集型任务,如图片处理、视频编解码、复杂算法等。如果这些任务在主线程执行,会长时间占用 CPU 导致界面卡顿,必须交给后台线程处理。
  • 最佳实践:在约 90% 的场景下,使用 Promise 和 async/await 会更简洁高效。

③ 异步和多线程的区别

  • "异步"是一种编程模式,关注让程序不因等待而阻塞。"多线程"是一种技术手段,通过创建额外的线程来并行执行代码,以利用多核 CPU 的计算能力。
  • 关系与比较:它们是不同维度的概念,异步编程可以在单线程中实现,而多线程是实现并发的具体方式。
  • 在鸿蒙中,Promise 和 async/await 是进行异步 I/O 操作的首选;而对于复杂计算任务,则应选用 TaskPool 或 Worker。

二、Promise 对象的核心机制

① Promise 状态和特性

  • Promise 是异步编程的一种解决方案,可以将其理解为一个状态机,它代表一个异步操作的最终完成(或失败)及其结果值。
  • Promise 的三种状态:
    • pending(进行中):初始状态,既没有被兑现,也没有被拒绝;
    • fulfilled(已成功):操作成功完成,并携带一个 result(结果值);
    • rejected(已失败):操作失败,并携带一个 reason(失败原因)。
  • Promise 的状态特性:
    • 不可逆性:状态一旦从 pending 变为 fulfilled 或 rejected,就会凝固,不能再发生任何改变;
    • 不可变性:Promise 的状态是内部的,外部代码无法读取或修改,只能通过 .then() 等方法注册回调来被动接收状态变化。

② Promise 的静态方法

  • Promise.all (a 和聚,一失全无):接收一个 Promise 数组,并行执行,所有 Promise 都 fulfilled,则返回包含所有结果的数组。只要有一个rejected,整个 Promise 立即 rejected,不再等待其他。适用场景:初始化一个需要多个独立数据源的页面,任何一个数据拉取失败都展示错误。
  • Promise.allSettled (和而不同):接收一个 Promise 数组,等待所有 Promise 都完成(无论成功还是失败),并返回一个包含每个 Promise 状态和结果/原因的对象数组,它永远不会 reject。适用场景:需要记录所有异步操作结果的场景,如批量埋点上报。
  • Promise.race (胜者为王):接收一个 Promise 数组,一旦数组中的任意一个 Promise 率先完成(无论是 fulfilled 还是 rejected),产生的 Promise 就会以该 Promise 的结果为结果。适用场景:为异步任务设置超时。
  • Promise.any (不求甚解):接收一个 Promise 数组,它会等待直到第一个 fulfilled 的 Promise 出现,如果所有 Promise 都 rejected,它才会 reject。适用场景:在多个备选 CDN 或服务中,选择最快响应的那个。

三、Promise 语法糖 async/await

① async 函数和 await 关键字的本质

  • async/await 是基于 Promise 的一层语法糖,目的是将异步代码的书写和阅读方式,变得更加接近同步代码,极大提升可读性和可维护性。
  • async:声明一个异步函数,它的返回值永远是一个 Promise 对象,async 函数的 return xxx 会被隐式地 Promise.resolve(xxx) 包装;而throw new Error() 则会被隐式地 Promise.reject(error) 包装。
  • await:只能在 async 函数内部使用,它会暂停当前 async 函数的执行,直到其右侧的 Promise 状态变更:
    • 若 Promise fulfilled,await 表达式返回 Promise 的结果值代码继续向下执行。
    • 若 Promise rejected,await 表达式会自动抛出异常,这也是必须在外部使用 try...catch 的原因。

② 对比 Promise 链式调用,async/await 的优势是什么?

  • async/await 能将多步异步逻辑"拉平",像同步代码一样从上到下顺序书写,而在 Promise 链中逻辑会散落在多个 .then() 块里。
  • async/await 能使用传统的 try/catch/finally 来包裹整个异步流程,同时捕获同步错误和异步错误。Promise 链中需要用 .catch() 或回调函数的第二个参数,并且可能遗漏同步错误。
  • 在编写 if/else 或 for 循环等条件分支时,async/await 表现自然,而在长的 Promise 链中插入分支会破坏链式结构,往往需要嵌套更多 .then()。
  • 现有如下的测试代码,输出结果是什么呢?
javascript 复制代码
async test() {
    console.log('A');
    await new Promise(resolve => {
        console.log('B');
        resolve();
    });
    console.log('C');
}
test();
console.log('D');
  • 运行这段代码会依次输出 A、B、D、C,关键在于,await 会让出线程,将其之后的代码(即微任务)添加到任务队列中等待执行。

四、async 的案例分析

① 能否将一个 async 匿名函数作为回调?

  • 在 ArkTS 中,能否将一个 async 匿名函数作为回调?例如 setTimeout(async () => {...}, 1000)?技术上是可以的,但需要特别注意,这样做的问题在于,外层代码(如 setTimeout)不会等待这个 async 回调完成。
  • async 回调内部的 await 只是暂停了该回调函数自身的执行,外层函数会直接返回,内部异步操作被丢到一边。因此,无法保证流程的顺序性。更可靠的方式是使用 Promise 显式地封装,或重构代码结构,避免在非异步回调中执行复杂的异步逻辑。

② 在 aboutToAppear 生命周期中,如果不小心写了一个未处理的 async 函数,会发生什么?

  • 这曾是一个隐蔽但破坏力很强的 bug,aboutToAppear 是页面显示前非常重要的生命周期,如果其中包含一个 async 函数而内部发生了未捕获的异常(Promise rejection),系统为维持自身稳定会"吞掉"这个异常。后果就是,异常之后的代码不会被执行,页面会卡住,但不崩溃,没有任何错误提示,UI 表现为白屏或部分内容缺失,非常难以调试。
  • 比较好的解决方案是,在所有 async 生命周期函数或回调函数中,务必使用 try...catch 包裹所有 await 操作。

③ async 的示例代码

  • 现有如下的测试代码,输出的顺序是什么呢?
javascript 复制代码
async task() {
    for (let i = 0; i < 3; i++) {
        await delay(1000);
        console.log(`任务完成: ${i}`);
    }
}
task()
  • 输出是每隔一秒依次打印:
javascript 复制代码
任务完成: 0
任务完成: 1
任务完成: 2
  • 这是因为在 for 循环的每次迭代中 await 都会暂停执行,导致循环是串行的,这是正确的预期行为。

五、拓展延伸

① async/await 能否替代 TaskPool 或 Worker 来实现真正的并行计算?

  • 这个绝对是不能的,async/await 在底层仍然运行在主线程上,引擎的主线程在等待异步操作时会被挂起,让出 CPU 给其他任务,但它本身不创建新的线程。因此,如果 await 后面跟的是一个耗时的同步计算(如一个长 for 循环),主线程依然会被阻塞,因为根本没有其他线程去执行这个计算。
  • TaskPool / Worker 这些是鸿蒙提供的多线程并发能力。它们会创建真正的、独立的操作系统线程,与主线程并行执行任务,这正是解决 CPU 密集型计算问题的唯一正确手段。

② Promise 构造函数内部的代码是同步还是异步执行的?

  • Promise 构造函数内部的代码是 同步执行的(类似于立即执行函数),而 .then()/.catch()/.finally() 里的回调才是异步(微任务)执行的。用一个例子可以清晰地说明:
javascript 复制代码
new Promise((resolve) => {
    console.log("A");
    resolve();
})
.then(() => console.log("B"));
console.log("C");
// 输出顺序为:A -> C -> B
  • 其中"A"立即输出,随后"C"输出,之后 .then 的回调在微任务队列执行输出"B"。

③ 避免主线程阻塞

javascript 复制代码
async myFunction() {
    console.log('A');
    await new Promise((resolve) => {
        for (let i = 0; i < 10000000; i++) {} // 耗时同步代码
        console.log('B');
    });
    console.log('C');
}
myFunction();
console.log('D');
  • 这个示例在于 Promise 构造函数内部的 for 循环是同步的,主线程会立即执行 myFunction(),打印 "A",接着执行 Promise 构造函数,由于 for 循环阻塞主线程,等待循环结束后才会打印 "B"。随后 await 机制生效,将 "C" 加入微任务队列,最后才打印 "D",因此最终输出顺序是 A -> B -> D -> C。这充分证明 Promise 构造器内的同步阻塞特性。
相关推荐
╰つ栺尖篴夢ゞ8 天前
HarmonyOS Next面试题之线程模型是如何确保UI操作在主线程中执行?
并发安全·线程模型·taskpool·eventhandler
切糕师学AI18 天前
深入浅出 协程(Coroutine):从原理到实践
高并发·协程·异步·async/await·coroutine·并发编程模型
叫我一声阿雷吧20 天前
JS 入门通关手册(43):async/await 原理与异常处理(实战 + 面试,彻底搞懂)
javascript·异常处理·promise·前端面试·async/await·generator·异步编程
叫我一声阿雷吧22 天前
JS 入门通关手册(42):Promise 并发控制(all/race/allSettled/any 手写 + 实战)
javascript·promise·并发控制·promise.all·js异步编程·promise.race·手写promise
牛奶25 天前
setTimeout设为0就马上执行?JS异步背后的秘密
前端·性能优化·promise
木斯佳1 个月前
前端八股文面经大全:bilibili前端一面(2026-03-26)·面经深度解析
前端·面试·笔试·校招·promise
乘方1 个月前
Promise/A+ 解析
promise
叫我一声阿雷吧1 个月前
JS 入门通关手册(24):Promise:从回调地狱到异步优雅写法
javascript·前端开发·promise·前端面试·异步编程·js进阶·js异步
前端小D1 个月前
ES6 中的 Promise
前端·javascript·es6·promise