--- JavaScript Promise 和 await 详解

JavaScript Promise 和 await 详解

深入解释什么是 Promise,为什么需要使用 await,以及在 Astro 框架中的应用场景

2025-09-03

作者:naiko

The Astro logo on a dark background with a pink glow.

javascript

promise

async-await

前端开发

astro

JavaScript Promise 和 await 详解

问题现象

在之前的文档 astro-glob-vs-import-meta-glob.md 的第50行,我们提到了:

- **需要使用 `await`**:因为它返回的是一个 Promise

你可能会问:什么是 Promise?为什么需要使用 await?本文将详细解释这些概念,并说明它们在 JavaScript 和 Astro 框架中的应用。

Promise 是什么?

基本概念

Promise 是 JavaScript 中用于处理异步操作的对象。它代表一个异步操作的最终完成(或失败)及其结果值。

简单来说,Promise 就像是一个 “承诺”:

Promise 的三种状态

一个 Promise 对象有三种可能的状态:

  1. pending(等待中):初始状态,既没有成功,也没有失败
  2. fulfilled(已成功):操作成功完成
  3. rejected(已失败):操作失败

一旦状态改变,就会永久保持该状态,不会再发生变化。这就是 Promise 的 不可变性

为什么需要 Promise?

在 Promise 出现之前,JavaScript 处理异步操作主要使用回调函数,这经常导致所谓的 “回调地狱”(Callback Hell)。

回调地狱的问题

假设有一个场景,你需要先获取用户信息,然后根据用户信息获取用户的订单,最后根据订单信息获取订单详情:

// 回调地狱的例子
getUserInfo(userId, function(userInfo) {
  getOrdersByUser(userInfo.id, function(orders) {
    getOrderDetails(orders[0].id, function(orderDetails) {
      // 处理订单详情
      console.log(orderDetails);
    }, function(error) {
      console.error('获取订单详情失败:', error);
    });
  }, function(error) {
    console.error('获取订单失败:', error);
  });
}, function(error) {
  console.error('获取用户信息失败:', error);
});

这种代码难以阅读、维护和调试。

Promise 的优势

使用 Promise 可以将上面的代码改写为链式调用,大大提高可读性:

// 使用 Promise 的例子
getUserInfo(userId)
  .then(userInfo => getOrdersByUser(userInfo.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(orderDetails => {
    // 处理订单详情
    console.log(orderDetails);
  })
  .catch(error => {
    console.error('发生错误:', error);
  });

await 是什么?

await 是 JavaScript 中用于等待 Promise 解决的关键字。它只能在 async 函数内部使用。

基本用法

async function processData() {
  try {
    const userInfo = await getUserInfo(userId);
    const orders = await getOrdersByUser(userInfo.id);
    const orderDetails = await getOrderDetails(orders[0].id);
    
    // 处理订单详情
    console.log(orderDetails);
  } catch (error) {
    console.error('发生错误:', error);
  }
}

这种写法看起来就像是同步代码,但实际上它在处理异步操作。

await 的作用

  1. 暂停执行await 会暂停当前 async 函数的执行,等待 Promise 解决
  2. 解包结果:如果 Promise 成功解决,await 会返回 Promise 的结果值
  3. 捕获错误:如果 Promise 被拒绝,await 会抛出错误,可以用 try/catch 捕获

为什么 Astro.glob 需要使用 await

回到我们的原始问题,为什么在使用 Astro.glob 时需要使用 await

技术原理

Astro.glob 是一个异步函数,它返回一个 Promise 对象。这是因为:

  1. 文件加载是异步的:在构建时或运行时,加载多个文件是一个耗时的操作
  2. Promise 提供了更好的错误处理:可以捕获和处理文件加载过程中可能出现的错误
  3. 不阻塞主线程:异步加载可以避免阻塞应用的渲染和交互

代码示例对比

不使用 await 的错误示例

// 错误用法
const allPosts = Astro.glob('../posts/*.md');

// 这里的 allPosts 是一个 Promise 对象,不是实际的文章数组
console.log(allPosts); // 输出: Promise { <pending> }

// 尝试访问 allPosts.map 会导致错误
// 因为 Promise 对象没有 map 方法
const tags = [...new Set(allPosts.map((post: any) => post.frontmatter.tags).flat())];

使用 await 的正确示例

// 正确用法
const allPosts = await Astro.glob('../posts/*.md');

// 这里的 allPosts 是实际的文章数组
console.log(allPosts); // 输出: [ { frontmatter: {...}, ... }, ... ]

// 现在可以正常使用 map 方法
const tags = [...new Set(allPosts.map((post: any) => post.frontmatter.tags).flat())];

Promise 的底层机制

如何创建 Promise?

你可以使用 new Promise() 构造函数来创建一个 Promise 对象:

const myPromise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    
    if (success) {
      resolve('操作成功!'); // 成功时调用 resolve
    } else {
      reject(new Error('操作失败!')); // 失败时调用 reject
    }
  }, 1000);
});

Promise 的方法

Promise 对象提供了几个有用的方法:

  1. then():处理 Promise 成功的情况
  2. catch():处理 Promise 失败的情况
  3. finally():无论 Promise 成功还是失败都会执行
  4. Promise.all():等待所有 Promise 都完成
  5. Promise.race():等待第一个完成的 Promise
  6. Promise.resolve():创建一个已解决的 Promise
  7. Promise.reject():创建一个已拒绝的 Promise

async/await 的工作原理

async 函数

async 函数是一个返回 Promise 的函数:

async function myFunction() {
  return 'Hello, world!';
}

// 等同于
function myFunction() {
  return Promise.resolve('Hello, world!');
}

await 的工作机制

当 JavaScript 引擎遇到 await 关键字时,它会:

  1. 暂时退出当前 async 函数,继续执行函数外部的代码
  2. 等待 Promise 解决或拒绝
  3. 如果 Promise 解决,将结果作为 await 表达式的值,然后继续执行 async 函数
  4. 如果 Promise 拒绝,抛出错误,可以用 try/catch 捕获

在 Astro 中的应用

除了 Astro.glob,还有哪些地方需要使用 await

在 Astro 项目中,你可能需要在以下场景使用 await

  1. 加载远程数据

    const data = await fetch('https://api.example.com/data').then(res => res.json());
  2. 使用 Astro 的 API

    const entry = await Astro.props.collection.getEntry('my-entry');
  3. 执行异步操作

    const result = await someAsyncFunction();

注意事项

在 Astro 中使用 await 时,需要注意以下几点:

  1. 只能在组件脚本部分使用await 只能在 .astro 文件的脚本部分(--- 之间)使用
  2. 不能在模板部分使用:在 HTML 模板部分不能直接使用 await
  3. 错误处理:使用 try/catch 来处理可能的错误

代码优化建议

1. 正确处理错误

// 好的做法:使用 try/catch 处理错误
let allPosts = [];

try {
  allPosts = await Astro.glob('../posts/*.md');
} catch (error) {
  console.error('加载文章失败:', error);
  // 可以提供默认数据或显示错误信息
}

2. 并行加载多个资源

如果多个异步操作之间没有依赖关系,可以使用 Promise.all() 来并行加载,提高性能:

// 并行加载多个资源
const [posts, tags, categories] = await Promise.all([
  Astro.glob('../posts/*.md'),
  Astro.glob('../tags/*.md'),
  Astro.glob('../categories/*.md')
]);

3. 使用类型定义

为 Promise 的结果添加类型定义,可以提高代码的可读性和可维护性:

interface Post {
  frontmatter: {
    title: string;
    pubDate: string;
    author: string;
    tags?: string[];
  };
  url: string;
}

const allPosts = await Astro.glob<Post>('../posts/*.md');

常见问题解答

Q: 我可以在普通函数中使用 await 吗? A: 不可以。await 关键字只能在 async 函数内部使用。如果你在普通函数中使用 await,会导致语法错误。

Q: async/await 和 Promise 有什么关系? A: async/await 是基于 Promise 的语法糖,它使异步代码的编写和阅读更加直观,就像同步代码一样。

Q: 为什么有时候 await 后面的代码没有执行? A: 这通常是因为 await 等待的 Promise 被拒绝了,但没有使用 try/catch 捕获错误。确保使用 try/catch 来处理可能的错误。

Q: 我可以 await 一个不是 Promise 的值吗? A: 可以。如果 await 后面不是 Promise,JavaScript 会隐式地将其包装在一个已解决的 Promise 中,然后立即返回该值。

Q: 在 Astro 中,await 会影响页面加载性能吗? A: 在服务器端渲染期间,await 可能会稍微增加页面的渲染时间,因为它需要等待异步操作完成。但是,对于获取关键数据来说,这通常是必要的。在客户端,异步操作不会阻塞页面的初始渲染。

总结

  1. Promise 是 JavaScript 中用于处理异步操作的对象,它有三种状态:pending、fulfilled 和 rejected

  2. await 是用于等待 Promise 解决的关键字,它只能在 async 函数内部使用

  3. Astro.glob 需要使用 await 是因为它返回一个 Promise 对象,使用 await 可以获取 Promise 解决后的实际结果

  4. 在 JavaScript 和 Astro 中,正确使用 Promise 和 await 可以帮助你编写更加清晰、易读和可维护的异步代码

通过理解这些概念,你将能够更好地处理 JavaScript 和 Astro 中的异步操作,避免常见的错误,并编写更加高效的代码。