JavaScript 是单线程语言,异步编程是其核心特性之一。从回调地狱到 Promise,再到 async/await 的语法糖,每一次进化都大幅提升了代码的可读性、可维护性和开发效率。理解 Promise 和 async/await 不仅是掌握现代 JavaScript 的必修课,更是深入理解事件循环、微任务、错误处理机制的关键。本文将系统剖析 Promise 的核心概念、链式调用、静态方法,以及 async/await 的底层原理和最佳实践,帮助你彻底掌握异步编程的精髓。

学习建议

        本文内容循序渐进,从基本用法到底层原理,建议按顺序阅读。如果你已经有 Promise 基础,可以直接跳至第三、四章,深入错误处理和高级模式;若对事件循环感兴趣,第六章将揭示微任务和宏任务的执行奥秘。


一、异步编程的演进:为什么需要 Promise?

1.1 同步与异步

                JavaScript 在浏览器中运行时,很多操作(如网络请求、文件读取、定时器)都是异步的——它们不会阻塞后续代码执行,而是通过回调函数在将来某个时刻通知结果。

// 同步代码:依次执行,会阻塞
console.log('第一步');
const result = doHeavyComputation(); // 假设耗时 2 秒
console.log('第二步'); // 必须等待上面完成

// 异步代码:不会阻塞
console.log('开始请求');
fetch('/api/data').then(response => {
  console.log('收到响应'); // 稍后执行
});
console.log('请求已发出,继续其他工作');

1.2 回调地狱

        早期异步编程完全依赖回调函数。当需要顺序执行多个异步操作时,就会出现层层嵌套的“回调地狱”:

getUser(1, (user) => {
  getOrders(user.id, (orders) => {
    getOrderDetails(orders[0].id, (details) => {
      sendEmail(details, () => {
        console.log('所有操作完成');
      });
    });
  });
});

这种代码难以阅读、调试和维护,错误处理更是噩梦。Promise 正是为了解决这些问题而诞生的。


二、Promise 核心概念与用法

2.1 什么是 Promise?

        Promise 是 ECMAScript 2015(ES6)引入的异步编程解决方案,它代表一个尚未完成但将来会完成的操作。Promise 对象有三种状态:

  • pending(进行中):初始状态,既未完成也未失败。
  • fulfilled(已成功):操作成功完成,并返回一个值。
  • rejected(已失败):操作失败,并返回一个错误原因。

        状态一旦从 pending 变为 fulfilled 或 rejected,就不可再变。这种“不可逆性”是 Promise 可靠性的基础。

2.2 创建一个 Promise

使用 new Promise 构造函数创建,传入执行器函数 (resolve, reject) => { ... }

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功');
    } else {
      reject(new Error('操作失败'));
    }
  }, 1000);
});
  • resolve(value):将状态从 pending 变为 fulfilled,并传递结果值。
  • reject(reason):将状态变为 rejected,并传递错误原因(通常是 Error 对象)。

2.3 消费 Promise:then、catch、finally

  • .then(onFulfilled, onRejected):注册成功和失败的回调函数,返回新的 Promise,实现链式调用。
  • .catch(onRejected):专门处理失败状态,相当于 .then(null, onRejected)
  • .finally(onFinally):无论成功或失败都会执行,适合清理资源(如关闭加载动画)。
const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('数据'), 500);
  });
};

fetchData()
  .then(data => {
    console.log(data); // '数据'
    return data.toUpperCase();
  })
  .then(upperData => {
    console.log(upperData); // '数据'
  })
  .catch(error => {
    console.error('出错:', error);
  })
  .finally(() => {
    console.log('请求结束(无论成败)');
  });

2.4 链式调用与值的传递

Promise 的精髓在于链式调用。.then() 返回一个新的 Promise,因此可以无限链下去。上一个 .then 的返回值会传递给下一个 .then 作为参数。

const asyncAdd = (a, b) => {
  return new Promise(resolve => {
    setTimeout(() => resolve(a + b), 100);
  });
};

asyncAdd(1, 2)
  .then(sum => asyncAdd(sum, 3))   // 返回 Promise
  .then(sum => asyncAdd(sum, 4))
  .then(sum => console.log(sum));   // 10

关键点:如果 .then 回调返回一个普通值,它会被隐式包裹成 Promise.resolve();如果返回一个 Promise,则下一个 .then 会等待其完成。

2.5 Promise 的静态方法

方法

说明

示例

Promise.resolve(value)

返回一个给定值的 resolved Promise

Promise.resolve(42).then(v => console.log(v))

Promise.reject(reason)

返回一个 rejected Promise

Promise.reject(new Error('fail')).catch(e => ...)

Promise.all(iterable)

所有 Promise 都成功时,返回一个包含所有结果的数组;一旦有一个失败,立即 reject

并发请求多个 API

Promise.allSettled(iterable)

等待所有 Promise 结束(无论成功或失败),返回每个对象的状态和结果

需要知道所有任务最终结果,不因个别失败而中断

Promise.race(iterable)

返回最先完成的 Promise 结果(成功或失败)

设置超时:Promise.race([fetch(url), timeout(5000)])

Promise.any(iterable)

返回第一个成功的 Promise;如果全部失败,则 reject 一个 AggregateError

多个备用服务,取最快成功的

// Promise.all 示例:等待多个异步操作
const urls = ['/user.json', '/posts.json'];
const promises = urls.map(url => fetch(url).then(res => res.json()));

Promise.all(promises)
  .then(([user, posts]) => {
    console.log('用户:', user, '帖子:', posts);
  })
  .catch(err => {
    console.error('至少一个请求失败', err);
  });

// Promise.race 实现超时
const fetchWithTimeout = (url, ms) => {
  const fetchPromise = fetch(url).then(res => res.json());
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('请求超时')), ms)
                                    );
  return Promise.race([fetchPromise, timeoutPromise]);
};

三、错误处理与最佳实践

3.1 捕获错误

在 Promise 链中,.catch 可以捕获上游任意位置抛出的错误或 reject。

doAsync()
  .then(step1)
  .then(step2)
  .catch(err => {
    // 能捕获 step1、step2 中同步抛出的错误,以及返回的 Promise 的 reject
  });

重要:如果某个 .then 没有提供错误处理函数,错误会一直向下传递直到被 .catch 捕获。因此,通常建议在链的末尾放置一个 .catch

3.2 抛出异常 vs 返回 rejected Promise

.then 回调中,throw new Error() 等价于返回 Promise.reject(new Error(...))。两者都会被后续的 .catch 捕获。

Promise.resolve()
  .then(() => {
    throw new Error('手动抛出');
  })
  .catch(err => console.log(err.message)); // '手动抛出'

Promise.resolve()
  .then(() => {
    return Promise.reject(new Error('返回 reject'));
  })
  .catch(err => console.log(err.message)); // '返回 reject'

3.3 常见陷阱:忘记 return 或 return Promise

链式调用中,如果忘记 return,下一个 .then 会接收到 undefined,导致意外行为。

// ❌ 错误:没有 return
fetchUser()
  .then(user => {
    fetchOrders(user.id);  // 没有 return,下一个 then 得到 undefined
  })
  .then(orders => {
    console.log(orders); // undefined,因为 fetchOrders 的 Promise 没有被链起来
  });

// ✅ 正确:return Promise
fetchUser()
  .then(user => {
    return fetchOrders(user.id);
  })
  .then(orders => {
    console.log(orders);
  });

3.4 全局未捕获的 Promise 拒绝

如果 Promise 被 reject 但没有 .catch 处理,可能会静默失败(早期浏览器)或抛出全局错误。现代 Node.js 和浏览器会触发 unhandledrejection 事件,建议始终添加 .catch

// Node.js 全局监听
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise 拒绝:', reason);
});

// 浏览器
window.addEventListener('unhandledrejection', event => {
  console.error('未处理的 Promise 拒绝:', event.reason);
});

四、async/await:用同步语法写异步代码

4.1 为什么引入 async/await?

Promise 虽然解决了回调地狱,但链式调用仍然有一定的心智负担,尤其是在处理分支循环或错误需要精细控制时。ES2017 引入了 asyncawait 关键字,让我们可以用同步代码的风格编写异步逻辑,同时不丧失 Promise 的所有优点。

4.2 async 函数

在函数声明前加上 async,该函数就会返回一个 Promise。函数内部可以包含 await 表达式。

async function getData() {
  return 42;
}
// 等价于
function getData() {
  return Promise.resolve(42);
}

getData().then(console.log); // 42

4.3 await 表达式

await 关键字只能在 async 函数内部使用。它会暂停当前函数的执行,等待右侧的 Promise 完成,然后返回 Promise 的结果(如果是失败,则抛出异常)。

async function fetchUserAndOrders(userId) {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    console.log(orders);
    return { user, orders };
  } catch (error) {
    console.error('请求失败', error);
    throw error; // 可选择重新抛出
  }
}

注意:await 后面的表达式如果不是 Promise,会被包装成 Promise.resolve

4.4 错误处理:try/catch

使用 async/await 时,可以使用传统的 try/catch 捕获错误,比 .catch 更加直观。

async function deleteRecord(id) {
  try {
    const res = await fetch(`/api/records/${id}`, { method: 'DELETE' });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    return data;
  } catch (err) {
    console.error('删除失败', err);
    // 可以执行补救操作或重新抛出
  }
}

4.5 并发执行:Promise.all 与 await

如果多个异步操作彼此没有依赖关系,应该让它们并发执行,而不是用 await 顺序等待。使用 Promise.all 可以同时发起多个请求。

// ❌ 低效:串行执行
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();

// ✅ 高效:并发执行
const [user, posts, comments] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchComments()
]);

4.6 循环中的 await

for 循环中使用 await 会顺序执行,如果希望并发,可以用 map + Promise.all

// 顺序处理(适用于有依赖关系的任务)
for (const id of ids) {
  const record = await fetchRecord(id);
  await processRecord(record);
}

// 并发处理(适用于无依赖)
const promises = ids.map(id => fetchRecord(id));
const records = await Promise.all(promises);

五、async/await 与 Promise 的互操作

5.1 将回调函数转换为 Promise

很多遗留 API 仍然使用回调(如 fs.readFile)。可以使用 util.promisify(Node.js)或手动包装成 Promise。

// Node.js
const fs = require('fs');
const { promisify } = require('util');
const readFileAsync = promisify(fs.readFile);

async function readConfig() {
  const content = await readFileAsync('./config.json', 'utf8');
  return JSON.parse(content);
}

// 手动包装
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

5.2 在 async 函数中直接返回 Promise

你可以在 async 函数中直接 return 一个 Promise,调用者仍然能获得正确的解析值。

async function getUser(id) {
  // 直接返回 fetch 的 Promise(fetch 本身返回 Promise)
  return fetch(`/users/${id}`).then(res => res.json());
}
// 等价于
async function getUser(id) {
  const res = await fetch(`/users/${id}`);
  return res.json();
}

5.3 顶层 await(Top-level await)

在 ES2022 中,模块中可以直接使用 await 而不需要 async 函数包装,极大方便了模块的初始化逻辑。但要注意它会阻塞模块加载,通常用于依赖异步资源的模块。

// db.js
const connection = await createDatabaseConnection();
export default connection;

// app.js
import db from './db.js'; // 会等待 db.js 中的顶层 await 完成

六、深入原理:事件循环与微任务

6.1 宏任务与微任务

Promise 的回调是微任务,而 setTimeoutsetInterval 等是宏任务。事件循环会在每个宏任务结束后,清空所有微任务队列,然后执行下一个宏任务。这解释了 Promise 为何比 setTimeout 更快触发。

setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
console.log('同步代码');
// 输出顺序:同步代码 → 微任务 → 宏任务

6.2 执行顺序的陷阱

Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  });

Promise.resolve()
  .then(() => {
    console.log('A');
  })
  .then(() => {
    console.log('B');
  })
  .then(() => {
    console.log('C');
  });
// 输出:1 A 2 B 3 C  (交替执行,因为每个 then 都产生一个新的微任务)

理解微任务队列的执行顺序,有助于避免异步 bug。


七、高级模式与实用技巧

7.1 实现重试机制

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i <= retries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (err) {
      if (i === retries) throw err;
      console.log(`第 ${i + 1} 次失败,重试中...`);
      await delay(1000 * (i + 1)); // 指数退避
    }
  }
}

7.2 限制并发数量

async function asyncPool(limit, array, iteratorFn) {
  const ret = [];
  const executing = [];
  for (const item of array) {
    const p = Promise.resolve().then(() => iteratorFn(item));
    ret.push(p);
    if (limit <= array.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(ret);
}

7.3 使用 AbortController 取消请求

async function fetchWithTimeout(url, timeoutMs) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(url, { signal: controller.signal });
    return await response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

7.4 惰性 Promise(Lazy Promise)

有时我们希望只在需要时才执行异步操作,可以封装一个工厂函数:

const lazyFetch = (url) => () => fetch(url).then(res => res.json());

const fetchUser = lazyFetch('/user.json');
// 稍后调用
const user = await fetchUser();

八、总结速查表

概念

核心要点

Promise 状态

pending → fulfilled / rejected,一旦确定不可变

then/catch/finally

链式调用,返回新 Promise;catch 捕获上游错误;finally 总是执行

静态方法

all

(全部成功)、allSettled

(等待全部结束)、race

(最快完成)、any

(最快成功)

async 函数

返回 Promise;内部可使用 await

await

暂停函数执行,等待 Promise 完成;可配合 try/catch 捕获错误

并发优化

使用 Promise.all

并发执行独立任务,避免串行 await

错误处理

始终在链式调用末尾加 .catch

,或在 async 函数中使用 try/catch

微任务

Promise 回调属于微任务,在宏任务之前执行

常见陷阱

忘记 return、未捕获的 reject、在循环中串行 await 独立任务

一句话记忆

        Promise 是异步操作的未来票根(成功或失败),而 async/await 是让我们用同步语法“等待”这张票根的糖衣炮弹——它们让 JavaScript 异步代码从回调地狱走向了声明式的优雅。


        掌握 Promise 和 async/await,你就能轻松驾驭现代 JavaScript 中几乎所有的异步场景——从网络请求到文件 I/O,从数据库操作到定时任务。建议你亲自编写几个实际例子,比如用 Promise.all 并发请求多个 API,用 async/await 改写一个回调噩梦,加深理解。这是通往高级前端/Node.js 开发的必经之路。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐