咱们在开发 HarmonyOS 应用的时候,经常会遇到这样的情况:比如发起网络请求后,得等服务器返回数据才能继续往下走;或者读取本地文件时,总不能让整个应用都卡着不动。这时候就需要用到异步并发能力了。今天咱们就好好聊聊 JavaScript 里处理异步的两大法宝 ——Promise 和 async/await,用最接地气的方式给大家讲明白,还附带实打实的 ArkTS 代码示例,保证看完就能用!

一、先搞懂:啥是异步并发?

说白了,异步并发就是程序执行的时候,不用傻等着一个操作完成,可以先去干别的活,等之前的操作有结果了再回来处理。这就好比咱们做饭,不用等水烧开了再去切菜,完全可以一边烧水一边准备食材,效率高多了!

重点总结

  • 异步操作不会阻塞后续代码执行,同一时间只执行一段代码
  • 典型场景:网络请求、文件读写、定时器等 I/O 操作
  • 适合处理轻量任务(单次执行时间短),且任务间有明确顺序或并行关系
  • 核心优势:提升程序响应速度,避免界面卡顿

举个最简单的例子,咱们用定时器模拟一个异步操作:

// 同步代码:会阻塞后续执行
console.log("开始烧水");
// 模拟烧水需要3秒(同步阻塞)
let start = Date.now();
while (Date.now() - start < 3000) {} 
console.log("水烧开了"); // 必须等3秒后才会执行
console.log("开始切菜"); // 被阻塞,只能在水开后执行

// 异步代码:不阻塞后续执行
console.log("开始烧水");
setTimeout(() => {
  console.log("水烧开了"); // 3秒后执行
}, 3000);
console.log("开始切菜"); // 不用等,立即执行

看,上面的同步代码里,切菜必须等水烧开才能开始;而异步代码里,烧水的同时就可以切菜,这就是异步的好处!

二、Promise:异步操作的 "状态管家"

咱们先认识第一个工具 ——Promise。它就像给异步操作装了个 "状态追踪器",能清楚地知道操作是正在进行、成功完成还是失败了,让异步代码变得更规整。

1. Promise 的三种状态:从 pending 到最终结果

Promise 有三个状态,就像快递的物流状态一样:

  • pending(进行中):刚创建 Promise 时的初始状态,就像快递刚发出
  • fulfilled(已完成,也叫 resolved):异步操作成功完成,类似快递签收
  • rejected(已拒绝):异步操作失败,比如快递丢失

重点总结

  • 状态一旦改变就不可逆:pending→fulfilled 或 pending→rejected 后,状态就固定了
  • 状态改变由异步操作结果决定:成功调用 resolve () 变 fulfilled,失败调用 reject () 变 rejected

来看个状态变化的例子:

// 创建一个判断随机数大小的Promise
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
  // 模拟1秒后完成的异步操作
  setTimeout(() => {
    const randomNum: number = Math.random(); // 生成0-1的随机数
    if (randomNum > 0.5) {
      resolve(randomNum); // 成功:状态变为fulfilled
    } else {
      reject(new Error("随机数太小啦!")); // 失败:状态变为rejected
    }
  }, 1000);
});

console.log("Promise刚创建时的状态:pending");
// 1秒后会根据随机数结果变成fulfilled或rejected

2. 用 then () 和 catch () 处理结果:给异步操作 "安排后事"

Promise 状态变了之后,咱们得知道结果啊!这时候就需要 then () 和 catch () 方法来处理成功或失败的情况。

重点总结

  • then () 可接收两个参数:第一个处理 fulfilled 状态,第二个处理 rejected 状态
  • catch () 专门处理 rejected 状态,相当于 then (null, 失败回调)
  • 回调函数会被加入微任务队列,等主线程空闲时执行

咱们给上面的 Promise 加上结果处理:

// 方法一:then()同时处理成功和失败
promise.then(
  (result: number) => {
    console.log(`成功!随机数是:${result}`); // 状态为fulfilled时执行
  },
  (error: Error) => {
    console.error(`失败!原因:${error.message}`); // 状态为rejected时执行
  }
);

// 方法二:then()处理成功,catch()处理失败(更推荐,代码更清晰)
promise
  .then((result: number) => {
    console.log(`成功!随机数是:${result}`);
  })
  .catch((error: Error) => {
    console.error(`失败!原因:${error.message}`);
  });

这里推荐用第二种方法,把成功和失败的处理分开写,代码看起来更清爽,也方便后续扩展。

3. 千万别漏了:未处理的 reject 会出大问题!

如果 Promise 被 reject 了,但咱们没写 catch () 处理,就会触发一个叫unhandledrejection的事件,这就好比快递丢了没人管,肯定会出问题。

重点总结

  • 未处理的 reject 会导致应用异常,甚至崩溃
  • 可以全局监听unhandledrejection事件,捕获漏网之鱼
// 全局监听未处理的Promise拒绝
import { errorManager } from '@kit.BasicServicesKit';

errorManager.on('unhandledrejection', (event: ErrorEvent) => {
  console.error(`捕获到未处理的错误:${event.error.message}`);
  event.preventDefault(); // 阻止默认处理(避免应用崩溃)
});

// 故意创建一个没有处理reject的Promise
new Promise((resolve, reject) => {
  reject(new Error("这个错误没被处理哦!"));
});

加上全局监听后,就算哪里不小心忘了写 catch (),也能及时发现问题,不至于让应用莫名其妙崩溃。

三、async/await:让异步代码像同步一样好写

虽然 Promise 已经比回调函数好用多了,但如果遇到多个依赖的异步操作,then () 链式调用还是有点绕。这时候 async/await 就登场了,它是 Promise 的 "语法糖",能让异步代码写起来跟同步代码一样直观。

1. 基本用法:async 声明函数,await 等待结果

重点总结

  • async关键字声明的函数,会自动返回一个 Promise
  • 函数内部用await关键字等待 Promise 完成,会暂停执行直到 Promise 状态改变
  • await 只能用在 async 函数里,不能直接用在普通函数中

咱们来看个例子,对比一下 Promise 和 async/await 的写法:

// 先用Promise写一个获取数据的函数
function fetchData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("这是从服务器拿到的数据");
    }, 2000);
  });
}

// 用Promise的then()处理
fetchData().then((data) => {
  console.log(`拿到数据:${data}`);
  return "处理后的数据";
}).then((processedData) => {
  console.log(`最终结果:${processedData}`);
});

// 用async/await处理(是不是清爽多了?)
async function handleData() {
  const data: string = await fetchData(); // 等待Promise完成,拿到结果
  console.log(`拿到数据:${data}`);
  const processedData: string = "处理后的数据";
  console.log(`最终结果:${processedData}`);
  return processedData; // async函数返回的是Promise
}

handleData();

看,用 async/await 之后,代码是从上到下顺序执行的,完全不用嵌套 then (),是不是跟写同步代码一样简单?

2. 错误处理:try/catch 捕获异常

await 后面的 Promise 如果被 reject 了,会抛出异常,这时候可以用 try/catch 来捕获,就像处理同步代码的错误一样。

重点总结

  • await 等待的 Promise 如果 reject,会触发异常
  • 用 try/catch 块包裹 await 操作,捕获可能的错误
  • 相当于 Promise 的 catch () 方法,但更符合同步代码的错误处理习惯
// 模拟一个可能失败的异步操作
function riskyOperation(): Promise<number> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = Math.random() > 0.5;
      if (success) {
        resolve(100); // 成功返回100
      } else {
        reject(new Error("操作失败啦!")); // 失败返回错误
      }
    }, 1500);
  });
}

// 用try/catch处理错误
async function doRiskyThing() {
  try {
    console.log("开始执行危险操作...");
    const result: number = await riskyOperation(); // 等待结果
    console.log(`操作成功!结果是:${result}`);
    return result;
  } catch (error: any) { // 捕获reject抛出的错误
    console.error(`操作失败:${error.message}`);
    return -1; // 返回默认值
  }
}

doRiskyThing();

这里用 try/catch 处理错误,比 Promise 的 catch () 更符合咱们平时写代码的习惯,一眼就能看出哪里可能出错,怎么处理。

3. 处理多个异步操作:并行和串行

实际开发中经常需要处理多个异步操作,有的要顺序执行(串行),有的可以同时执行(并行),用 async/await 处理起来很灵活。

重点总结

  • 串行:多个 await 依次写,前一个完成再执行后一个
  • 并行:用 Promise.all () 包装多个 Promise,再用 await 等待,效率更高
// 模拟两个异步操作
function asyncTask1(): Promise<number> {
  return new Promise(resolve => setTimeout(() => resolve(1), 1000));
}

function asyncTask2(): Promise<number> {
  return new Promise(resolve => setTimeout(() => resolve(2), 2000));
}

// 1. 串行执行:总耗时≈1+2=3秒
async function serialTasks() {
  console.log("开始串行执行...");
  const start = Date.now();
  
  const result1 = await asyncTask1(); // 等1秒
  const result2 = await asyncTask2(); // 再等2秒
  
  const totalTime = (Date.now() - start) / 1000;
  console.log(`串行结果:${result1}, ${result2},耗时${totalTime}秒`);
}

// 2. 并行执行:总耗时≈2秒(取最长的那个)
async function parallelTasks() {
  console.log("开始并行执行...");
  const start = Date.now();
  
  // 同时启动两个任务
  const promise1 = asyncTask1();
  const promise2 = asyncTask2();
  
  // 等待两个都完成
  const [result1, result2] = await Promise.all([promise1, promise2]);
  
  const totalTime = (Date.now() - start) / 1000;
  console.log(`并行结果:${result1}, ${result2},耗时${totalTime}秒`);
}

serialTasks();
setTimeout(parallelTasks, 4000); // 等串行执行完再跑并行

从结果能明显看出,并行执行比串行快很多。所以如果多个异步操作之间没有依赖关系,一定要用并行的方式,能大大提高效率。

4. 注意事项:这些坑千万别踩!

虽然 async/await 很好用,但有几个注意点得记牢,不然容易出问题:

重点总结

  • await 会暂停当前 async 函数,但不会阻塞其他代码
  • 不要在循环里用 await(会变成串行,效率低)
  • 多个无依赖的 await 要改用 Promise.all ()
// 反面例子:循环里用await,导致串行执行
async function badLoop() {
  const tasks = [1, 2, 3];
  const results: number[] = [];
  
  for (const task of tasks) {
    // 每次都等上一个完成,总耗时≈3秒
    const result = await new Promise(resolve => 
      setTimeout(() => resolve(task * 10), 1000)
    );
    results.push(result);
  }
  
  console.log("循环结果(串行):", results);
}

// 正面例子:用Promise.all()并行处理循环任务
async function goodLoop() {
  const tasks = [1, 2, 3];
  
  // 先创建所有Promise(同时启动)
  const promises = tasks.map(task => 
    new Promise(resolve => 
      setTimeout(() => resolve(task * 10), 1000)
    )
  );
  
  // 等待所有完成(总耗时≈1秒)
  const results = await Promise.all(promises);
  console.log("循环结果(并行):", results);
}

badLoop();
setTimeout(goodLoop, 4000);

同样是处理 3 个任务,坏例子用了 3 秒,好例子只用了 1 秒,效率差了 3 倍!所以写循环的时候一定要注意这个问题。

四、实战场景:在 HarmonyOS 组件中用异步

咱们开发 HarmonyOS 应用时,经常需要在 UI 组件里处理异步操作(比如点击按钮后请求数据)。下面就用一个实际场景,看看怎么把 Promise 和 async/await 用到组件里。

场景:点击按钮后,先请求用户信息,再用用户 ID 请求用户订单,最后显示结果。

@Entry
@Component
struct AsyncDemoPage {
  @State message: string = "点击按钮加载数据";
  @State loading: boolean = false;

  // 模拟请求用户信息(1秒返回)
  private fetchUserInfo(): Promise<{ id: number; name: string }> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ id: 1001, name: "小明" });
      }, 1000);
    });
  }

  // 模拟用用户ID请求订单(2秒返回)
  private fetchOrders(userId: number): Promise<{ id: number; total: number }[]> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve([
          { id: 1, total: 99 },
          { id: 2, total: 199 }
        ]);
      }, 2000);
    });
  }

  // 处理点击事件的异步函数
  private async handleClick() {
    this.loading = true;
    this.message = "加载中...";

    try {
      // 先获取用户信息
      const user = await this.fetchUserInfo();
      // 再用用户ID获取订单(依赖上一步结果,必须串行)
      const orders = await this.fetchOrders(user.id);
      
      // 显示结果
      this.message = `${user.name}的订单:共${orders.length}个,总金额${orders.reduce((sum, o) => sum + o.total, 0)}元`;
    } catch (error: any) {
      this.message = `加载失败:${error.message}`;
    } finally {
      this.loading = false;
    }
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(16)
          .margin(20)
        
        Button(this.loading ? "加载中..." : "点击加载数据")
          .fontSize(16)
          .padding({ left: 20, right: 20 })
          .onClick(() => this.handleClick())
          .enabled(!this.loading) // 加载中禁用按钮
      }
      .width('100%')
      .height('100%')
    }
  }
}

这个例子里,咱们用 async/await 把两个有依赖的异步操作(先用户信息,后订单)写得像同步代码一样直观,还加了 loading 状态防止重复点击,用 try/catch 处理可能的错误,非常实用。

五、总结:Promise 和 async/await 怎么选?

看到这里,可能有同学会问:既然 async/await 这么好用,那是不是可以不用 Promise 了?其实不是的,它们俩是相辅相成的关系:

  • Promise 是基础:async/await 本质上还是基于 Promise 的,所有 async 函数都返回 Promise
  • 简单场景用 Promise:单个异步操作,用 then () 更简洁
  • 复杂场景用 async/await:多个依赖的异步操作,或者需要和同步代码混写时,async/await 更清晰

核心记住这几点

  1. 异步操作用 Promise 包装,明确状态和结果
  2. 单个异步用 then ()/catch (),多个依赖用 async/await
  3. 并行操作优先用 Promise.all (),提高效率
  4. 别忘全局监听 unhandledrejection,兜底防崩溃

掌握了这两个工具,不管是处理网络请求、文件读写还是定时器,都能游刃有余,写出既高效又好维护的代码。赶紧在自己的项目里试试吧,实践几次就能熟练掌握啦!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐