JavaScript Promise 与 async/await 完全指南:异步编程的进化之路
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 的静态方法
|
方法 |
说明 |
示例 |
|
|
返回一个给定值的 resolved Promise |
|
|
|
返回一个 rejected Promise |
|
|
|
所有 Promise 都成功时,返回一个包含所有结果的数组;一旦有一个失败,立即 reject |
并发请求多个 API |
|
|
等待所有 Promise 结束(无论成功或失败),返回每个对象的状态和结果 |
需要知道所有任务最终结果,不因个别失败而中断 |
|
|
返回最先完成的 Promise 结果(成功或失败) |
设置超时: |
|
|
返回第一个成功的 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 引入了 async 和 await 关键字,让我们可以用同步代码的风格编写异步逻辑,同时不丧失 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 的回调是微任务,而 setTimeout、setInterval 等是宏任务。事件循环会在每个宏任务结束后,清空所有微任务队列,然后执行下一个宏任务。这解释了 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 总是执行 |
|
静态方法 |
(全部成功)、 (等待全部结束)、 (最快完成)、 (最快成功) |
|
async 函数 |
返回 Promise;内部可使用 await |
|
await |
暂停函数执行,等待 Promise 完成;可配合 try/catch 捕获错误 |
|
并发优化 |
使用 并发执行独立任务,避免串行 await |
|
错误处理 |
始终在链式调用末尾加 ,或在 async 函数中使用 try/catch |
|
微任务 |
Promise 回调属于微任务,在宏任务之前执行 |
|
常见陷阱 |
忘记 return、未捕获的 reject、在循环中串行 await 独立任务 |
一句话记忆:
Promise 是异步操作的未来票根(成功或失败),而 async/await 是让我们用同步语法“等待”这张票根的糖衣炮弹——它们让 JavaScript 异步代码从回调地狱走向了声明式的优雅。
掌握 Promise 和 async/await,你就能轻松驾驭现代 JavaScript 中几乎所有的异步场景——从网络请求到文件 I/O,从数据库操作到定时任务。建议你亲自编写几个实际例子,比如用 Promise.all 并发请求多个 API,用 async/await 改写一个回调噩梦,加深理解。这是通往高级前端/Node.js 开发的必经之路。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)