前言
JavaScript 是单线程的语言,但 Web 开发中充斥着大量异步操作:网络请求、文件读写、定时器…… 传统的回调函数(Callback)在处理多个异步任务时容易陷入回调地狱1回调地狱(Callback Hell)指嵌套多层回调函数导致代码难以阅读和维护的现象。Promise 通过链式调用解决了这个问题。(Callback Hell),代码难以维护。Promise 应运而生,它提供了一种更优雅的方式来组织异步代码。
本文综合参考了 MDN Web Docs、JavaScript.info 等资料,从零开始整理 Promise 的核心知识点。
什么是 Promise
Promise(期约)是一个代表异步操作最终完成(或失败)及其结果值的对象。它是异步操作结果的占位符 —— 就像去餐厅取餐时拿到的一个叫号器,等餐做好了它会通知你。
| 术语 | 英文 | 含义 |
|---|---|---|
| 待定 | pending | 初始状态,操作尚未完成 |
| 已兑现 | fulfilled | 操作成功完成,有返回值 |
| 已拒绝 | rejected | 操作失败,有错误原因 |
| 已定型 | settled | 已经兑现或拒绝,不再变动 |
一个 Promise 对象处于以下三种状态之一:
- pending(待定):初始状态,既没有完成也没有拒绝。
- fulfilled(已兑现):操作成功完成。
- rejected(已拒绝):操作失败。
当 Promise 从 pending 变为 fulfilled 或 rejected 后,它的状态就不再改变2Promise 状态是单向且不可逆的:pending → fulfilled 或 pending → rejected。一旦状态改变(settled),就永久锁定,后续的 resolve/reject 调用会被忽略。。这被称为 settled(已定型)。
const promise = new Promise((resolve, reject) => { // 异步操作... if (/* 成功 */) { resolve(value); } else { reject(error); }});Promise 的状态只能改变一次,一旦从 pending 变为 fulfilled 或 rejected,就永久锁定。后续的 resolve 或 reject 调用都会被忽略。
创建 Promise
Promise 构造函数
Promise 构造函数接受一个执行器(executor)函数3Promise 构造函数的 executor 函数由 JavaScript 引擎立即同步执行。异步操作应在 executor 内部启动,通过 resolve/reject 改变 Promise 状态。作为参数。执行器函数由 JavaScript 引擎同步执行,负责启动异步操作。
const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('完成!'); }, 1000);});执行器函数接收两个参数:
resolve(value):操作成功时调用,将 Promise 状态改为 fulfilled。reject(error):操作失败时调用,将 Promise 状态改为 rejected。
const promise = new Promise((resolve, reject) => { resolve('第一次'); resolve('第二次'); // 忽略 reject(new Error('错误')); // 忽略});建议在 reject 时总是传入 Error 对象(或其子类),而非普通字符串 —— 这样能得到完整的调用栈信息,便于调试。
Promise.resolve() 与 Promise.reject()
快速创建已定型 Promise:
const resolved = Promise.resolve(42);const rejected = Promise.reject(new Error('失败'));Promise.resolve 尤其适合在函数需要统一返回 Promise 时使用,也可以将 thenable4Thenable 是指拥有 .then() 方法的对象。Promise.resolve() 可以将 thenable 转换为标准 Promise,这是 Promise 互操作性的基础。 转换为标准 Promise,比如缓存场景:
const cache = new Map();
function loadCached(url) { if (cache.has(url)) { return Promise.resolve(cache.get(url)); } return fetch(url).then(r => r.text()).then(text => { cache.set(url, text); return text; });}消费 Promise:then / catch / finally
| 方法 | 用途 | 等价写法 |
|---|---|---|
.then(onFulfilled, onRejected) | 同时处理成功和失败 | 基础方法 |
.catch(onRejected) | 只处理失败 | .then(null, onRejected) |
.finally(onFinally) | 无论成败都要执行 | .then(f, f) 但略有差异 |
then()
.then() 是 Promise 最核心的方法,它接受两个回调参数:
promise.then( (result) => { /* 处理成功 */ }, (error) => { /* 处理失败 */ });const promise = new Promise((resolve) => { setTimeout(() => resolve('done!'), 1000);});
promise.then( result => console.log(result), // 1秒后输出 "done!" error => console.log(error) // 不会执行);如果只关心成功结果,可以只传一个参数:
promise.then(console.log);catch()
.catch(f) 等价于 .then(null, f),专门用来处理错误:
const promise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Whoops!')), 1000);});
promise.catch(error => console.log(error.message)); // "Whoops!"finally()
.finally(f) 不论 Promise 成功还是失败都会执行,适合做清理工作(如关闭加载动画、释放资源):
showLoading();fetch(url) .then(response => response.json()) .catch(error => console.error(error)) .finally(() => hideLoading());.finally 的特殊之处:
| 特性 | 说明 |
|---|---|
| 无参数 | 不知道 Promise 结果,只用于清理 |
| 透传结果 | 将 value 或 error 原样交给下一级 |
| 不可改变状态 | 除非 finally 内部抛出异常 |
.finally(f) 不等同于 .then(f, f):前者不接收参数且结果会透传,后者会接收结果值并可能改变链式传递的值。
链式调用(Promise Chaining)5Promise 链式调用的关键:每个 .then() 返回一个新 Promise。如果回调返回值,该值被包装为 fulfilled Promise;如果返回 Promise,则等待其完成。
链式调用是 Promise 最大的亮点之一。每次调用 .then() 都会返回一个新的 Promise,因此可以无限链式调用下去。
new Promise((resolve) => { setTimeout(() => resolve(1), 1000);}) .then(result => { console.log(result); // 1 return result * 2; }) .then(result => { console.log(result); // 2 return result * 2; }) .then(result => { console.log(result); // 4 });链式 vs 分支
初学者容易混淆这两个概念。它们完全不同:
| 模式 | 多个 .then() 在一个链上 | 多个 .then() 在同一个 Promise 上 |
|---|---|---|
| 结果传递 | 上游结果 → 下游 | 各自独立接收相同值 |
| 实际用途 | 串联异步操作 | 并联多个独立回调 |
const p = new Promise(resolve => resolve(1));
// ✅ 链式 — 值层层传递p.then(r => r * 2).then(r => console.log(r)); // 2
// ❌ 分支 — 三个处理器各自独立,都得到 1p.then(r => console.log(r)); // 1p.then(r => console.log(r)); // 1p.then(r => console.log(r)); // 1返回 Promise 的链式调用
在 .then() 中返回一个 Promise,下一个 .then() 会等待它完成:
new Promise((resolve) => { setTimeout(() => resolve(1), 1000);}) .then(result => { console.log(result); // 1 return new Promise((resolve) => { setTimeout(() => resolve(result * 2), 1000); }); }) .then(result => { console.log(result); // 2 });与 fetch 结合的实际例子
fetch('/api/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => { const img = document.createElement('img'); img.src = githubUser.avatar_url; document.body.append(img); return new Promise((resolve) => { setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); }); }) .then(githubUser => console.log(`已完成展示 ${githubUser.name}`));一个良好的实践:异步操作应该始终返回 Promise。这样即使现在不需要扩展链,将来也可以轻松地在后面添加更多操作。上面的例子通过返回一个 Promise 来控制下一级 .then() 的执行时机。
错误处理
隐式 try…catch
Promise 的执行器和处理器内部有一个隐式 try…catch。Promise 回调进入微任务队列6Promise 的 .then/.catch/.finally 回调进入微任务队列(Microtask Queue),优先级高于宏任务(setTimeout/setInterval)。当前宏任务完成后,会先清空微任务队列。。如果抛出异常,会自动转为 rejected Promise:
new Promise((resolve, reject) => { throw new Error('Whoops!');}).catch(console.log); // Error: Whoops!
// 等价于new Promise((resolve, reject) => { reject(new Error('Whoops!'));}).catch(console.log);这种隐式捕获不仅作用于 executor,也作用于 .then() 处理器:
new Promise((resolve, reject) => { resolve('ok');}) .then(result => { throw new Error('then 中出错'); }) .catch(console.log); // Error: then 中出错错误传播
在链式调用中,只要有一个 Promise 被 rejected,控制权就跳转到最近的 rejection 处理器:
fetch('https://no-such-server.example.com') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('捕获错误:', error));重新抛出(Rethrowing)
在 .catch() 内部,如果错误无法处理,可以重新抛出,交给链中更下游的 .catch():
new Promise((resolve, reject) => { throw new Error('Whoops!');}) .catch(error => { if (error instanceof URIError) { // 处理特定类型错误 } else { console.log('无法处理此类错误'); throw error; // 重新抛出 } }) .catch(error => { console.log(`未知错误: ${error}`); });好的错误处理习惯:在 .catch() 中识别已知错误,重新抛出未知错误。可以让链末端的最后一个 .catch() 处理非预期的异常,或者交给全局错误处理器。
未捕获的拒绝(Unhandled Rejection)
如果 Promise 被 rejected 却没有对应的 .catch() 处理,JavaScript 引擎会触发全局事件:
window.addEventListener('unhandledrejection', (event) => { console.log(`未处理的 Promise 错误: ${event.reason}`); // 可以在此上报错误到服务器});在 Node.js 中对应的是 unhandledRejection 事件。
Promise 静态方法
Promise 类提供 6 个静态方法,下面逐一详解。
Promise.all()7Promise.all() 并行执行多个 Promise,全部成功才成功,任何一个失败立即失败。返回结果数组顺序与输入顺序一致,而非完成顺序。
并行执行多个 Promise,等待全部完成。如果任何一个被 rejected,整体立即失败。
const promises = [ fetch('/api/users'), fetch('/api/posts'), fetch('/api/comments'),];
Promise.all(promises) .then(([users, posts, comments]) => { console.log('所有数据加载完成'); }) .catch(error => { console.error('某个请求失败:', error); });结果数组的顺序与输入数组一致,即使某个 Promise 更慢:
Promise.all([ new Promise(r => setTimeout(() => r(1), 3000)), new Promise(r => setTimeout(() => r(2), 2000)), new Promise(r => setTimeout(() => r(3), 1000)),]).then(alert); // 1, 2, 3 — 保持输入顺序如果一个 Promise 被 rejected,其他 Promise 不会自动取消,它们仍然会继续执行,只是结果被忽略。这在一些场景下可能导致意外的并发行为(见后面常见误区)。
Promise.all 也接受非 Promise 值,它们会”原样”通过:
Promise.all([ new Promise(r => setTimeout(() => r(1), 1000)), 2, 3,]).then(console.log); // [1, 2, 3]Promise.allSettled()
等待所有 Promise 结束(无论成功还是失败),返回每个 Promise 的结果和状态:
const promises = [ fetch('/api/users'), fetch('/no-such-url'), fetch('/api/posts'),];
Promise.allSettled(promises).then(results => { results.forEach((result, i) => { if (result.status === 'fulfilled') { console.log(`请求 ${i} 成功:`, result.value.status); } else { console.log(`请求 ${i} 失败:`, result.reason); } });});返回格式:
[ { "status": "fulfilled", "value": ... }, { "status": "rejected", "reason": ... }]如果浏览器不支持 Promise.allSettled,可以使用以下 polyfill:
if (!Promise.allSettled) { const rejectHandler = reason => ({ status: 'rejected', reason }); const resolveHandler = value => ({ status: 'fulfilled', value });
Promise.allSettled = function (promises) { const converted = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler) ); return Promise.all(converted); };}Promise.race()
等待第一个 settled 的 Promise,以它的结果为准(无论成功还是失败):
Promise.race([ fetch('/api/fast'), fetch('/api/slow'),]).then(response => { console.log('最快的请求返回了');});Promise.any()
等待第一个 fulfilled 的 Promise。如果全部被 rejected,则返回 AggregateError:
Promise.any([ fetch('/api/might-fail-1'), fetch('/api/might-fail-2'), fetch('/api/might-fail-3'),]).then(response => { console.log('至少有一个请求成功');}).catch(error => { console.log('所有请求都失败了:', error.errors);});静态方法完整对比
| 方法 | 等待行为 | 成功条件 | 失败条件 | 典型场景 |
|---|---|---|---|---|
Promise.all | 等待全部 | 全部成功 | 任意一个失败 | 必须全部完成的并行请求 |
Promise.allSettled | 等待全部 | 总是成功 | 永不失败 | 各请求独立,不在意个别失败 |
Promise.race | 等待第一个 settled | 第一个成功 | 第一个失败 | 超时控制 |
Promise.any | 等待第一个 fulfilled | 第一个成功 | 全部失败 | 任意资源获取,谁快用谁 |
Promise.resolve / reject
| 方法 | 行为 | 等价代码 |
|---|---|---|
Promise.resolve(value) | 创建一个 fulfilled 的 Promise | new Promise(r => r(value)) |
Promise.reject(error) | 创建一个 rejected 的 Promise | new Promise((_, r) => r(error)) |
Thenable(可 then 对象)
Promise 的互操作性基于 .then() 方法。任何实现了 .then() 方法的对象都被称为 thenable,可以被 Promise 的处理机制识别:
class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { setTimeout(() => resolve(this.num * 2), 1000); }}
Promise.resolve(1) .then(result => new Thenable(result)) .then(console.log); // 1秒后输出 2这意味着第三方库可以实现自己的”Promise 兼容”对象,而无须继承 Promise。只要对象有 .then() 方法,就能与 JavaScript 原生的 Promise 机制无缝协作。
async / await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码。
async 关键字
async 声明的函数总是返回一个 Promise:
async function greet() { return 'Hello!';}
greet().then(console.log); // "Hello!"
// 等价于function greet() { return Promise.resolve('Hello!');}| 返回类型 | 转换方式 |
|---|---|
| 非 Promise 值 | 自动包装为 Promise.resolve(value) |
| Promise | 原样返回 |
| throw 异常 | 转为 Promise.reject(error) |
await 关键字
await 等待一个 Promise 定型,并返回其结果。它只能在 async 函数中使用:
async function getUserData() { const response = await fetch('/api/user'); const user = await response.json(); return user;}// ❌ SyntaxError:不能在普通函数中使用 awaitfunction f() { let result = await Promise.resolve(1); // 报错}改用 async/await 重写 fetch 链式调用
将前面的链式示例改为 async/await:
async function showAvatar() { const response = await fetch('/api/user.json'); const user = await response.json();
const githubResponse = await fetch(`https://api.github.com/users/${user.name}`); const githubUser = await githubResponse.json();
const img = document.createElement('img'); img.src = githubUser.avatar_url; document.body.append(img);
await new Promise(r => setTimeout(r, 3000)); img.remove();
return githubUser;}
showAvatar().then(user => console.log('完成'));错误处理
在 async/await 中,可以使用 try…catch 捕获错误:
async function loadUser() { try { const response = await fetch('/api/user'); const user = await response.json(); return user; } catch (error) { console.error('加载用户失败:', error); throw error; }}如果不使用 try…catch,错误会作为 Promise 的 rejection 传播:
async function loadUser() { const response = await fetch('/api/user'); const user = await response.json(); return user;}
loadUser().catch(error => console.error(error));async/await 与 try…catch 的组合比 .then().catch() 链式更加直观,尤其在需要处理多个 await 表达式的错误时,可以用一个 try 块包裹全部。
顶层 await
在 ES2022 中,模块的顶层可以直接使用 await:
// 仅在模块中有效const config = await fetch('/config.json').then(r => r.json());console.log(config);如果不是模块环境,可以使用 IIFE(立即执行函数表达式)来模拟:
(async () => { const response = await fetch('/api/data'); const data = await response.json(); console.log(data);})();await 与 Promise.all
并行等待多个 Promise 的正确方式:
// ❌ 错误:串行执行(一个等一个)const user = await fetch('/api/user');const posts = await fetch('/api/posts');
// ✅ 正确:并行执行const [user, posts] = await Promise.all([ fetch('/api/user'), fetch('/api/posts'),]);学习路线图
你可以按照以下顺序逐步掌握 Promise:
- 理解回调地狱的问题
- 掌握 Promise 基础与三种状态
- 学会 then / catch / finally 的用法
- 理解链式调用 vs 分支的区别
- 掌握 Promise.all / allSettled / race / any
- 学习 async / await 语法糖
- 掌握错误处理的最佳实践
- 了解微任务队列(Microtask Queue)原理
- 在实际项目中应用并发控制模式
实际应用场景
将回调函数 Promise 化
function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`脚本加载失败: ${src}`)); document.head.append(script); });}
loadScript('/js/app.js') .then(() => console.log('脚本已加载')) .catch(error => console.error(error));超时控制(Promise.race)
function fetchWithTimeout(url, timeout = 5000) { return Promise.race([ fetch(url), new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时')), timeout) ), ]);}重试机制
async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url); if (response.ok) return response.json(); } catch (error) { if (i === retries - 1) throw error; console.log(`重试第 ${i + 1} 次...`); } }}并发控制
async function parallelLimit(tasks, limit) { const results = []; const executing = new Set();
for (const task of tasks) { const p = Promise.resolve().then(() => task()); results.push(p); executing.add(p);
const clean = () => executing.delete(p); p.then(clean, clean);
if (executing.size >= limit) { await Promise.race(executing); } }
return Promise.all(results);}
// 限制最多 3 个并发请求const urls = [...];const tasks = urls.map(url => () => fetch(url));parallelLimit(tasks, 3).then(responses => { console.log('全部完成');});常见误区
误区 1:链式调用时忘记 return
// ❌ 错误:没有 return,下一个 then 收不到数据fetch('/api/user') .then(response => { response.json(); // 缺少 return }) .then(user => { console.log(user); // undefined });
// ✅ 正确fetch('/api/user') .then(response => response.json()) // 隐式 return .then(user => console.log(user));误区 2:在同步代码中使用 Promise 判断状态
const p = Promise.resolve(42);console.log(p.state); // ❌ 无法访问内部状态console.log(p.result); // ❌ 无法直接访问结果Promise 的 state 和 result 是内部属性,无法直接从外部读取,只能通过 .then() / .catch() 获取。
误区 3:错误地被 finally 吞掉
// ❌ 如果 finally 内部抛出错误,会覆盖原有错误Promise.reject(new Error('原始错误')) .finally(() => { throw new Error('finally 错误'); }) .catch(error => console.log(error.message)); // "finally 错误"误区 4:回调写法嵌套(Pyramid of Doom)
// ❌ 错误:仍然嵌套,退化为回调地狱loadScript('a.js').then(() => { loadScript('b.js').then(() => { loadScript('c.js').then(() => { console.log('全部加载完成'); }); });});
// ✅ 正确:扁平链式loadScript('a.js') .then(() => loadScript('b.js')) .then(() => loadScript('c.js')) .then(() => console.log('全部加载完成'));误区 5:Promise.all 中一个失败其他不会取消
// 即使一个请求失败,其他请求仍然在飞Promise.all([ fetch('/api/a'), fetch('/api/b'), // 这个会失败 fetch('/api/c'), // 这个仍然会发送并执行,但其结果/错误被忽略]).catch(error => { console.error(error);});误区 6:then 的第二个参数 vs catch
// ⚠️ 区别:promise.then(f1, f2) vs promise.then(f1).catch(f2)// 前者不会捕获 f1 内部的错误,后者会!promise.then(f1, f2); // f1 抛出错误 → 不会进入 f2promise.then(f1).catch(f2); // f1 抛出错误 → 进入 f2高级话题:微任务队列
Promise 的处理基于 JavaScript 的事件循环(Event Loop)中的微任务队列(Microtask Queue):
| 队列 | 优先级 | 示例 |
|---|---|---|
| 宏任务(Macrotask) | 低 | setTimeout、setInterval、IO |
| 微任务(Microtask) | 高 | Promise .then()、queueMicrotask() |
setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
console.log('同步代码');
// 输出顺序:// 同步代码 → 微任务 → 宏任务理解微任务队列对调试异步代码非常关键。Promise 的回调总是在当前宏任务结束、下一个宏任务开始之前执行,这解释了为什么 Promise.resolve().then() 的优先级高于 setTimeout(fn, 0)。
浏览器兼容性与 Polyfill
| 特性 | ES 版本 | 支持范围 |
|---|---|---|
Promise 基础 | ES2015 | 所有现代浏览器 |
Promise.all / race | ES2015 | 所有现代浏览器 |
Promise.allSettled | ES2020 | 现代浏览器,需 polyfill 较旧环境 |
Promise.any | ES2021 | 现代浏览器,需 polyfill 较旧环境 |
AggregateError | ES2021 | 与 Promise.any 同时支持 |
async/await | ES2017 | 所有现代浏览器 |
| 顶层 await | ES2022 | 现代浏览器的模块上下文 |
如需兼容老旧浏览器,可使用 core-js 等 polyfill:
npm install core-jsimport 'core-js/actual/promise';import 'core-js/actual/promise/all-settled';总结
JavaScript Promise 是处理异步编程的核心机制,它解决了回调地狱的问题。通过这张表可以快速回顾全文要点:
| 知识点 | 要点 | 常用方法 |
|---|---|---|
| Promise 基础 | 三个状态:pending / fulfilled / rejected | new Promise() |
| 消费结果 | then 接收结果,catch 捕获错误,finally 清理 | .then() .catch() .finally() |
| 链式调用 | 每次 then 返回新 Promise,扁平化异步流程 | .then(() => ...).then(() => ...) |
| 并发控制 | 四种静态方法应对不同并行场景 | all / allSettled / race / any |
| 错误处理 | 隐式 try…catch,可重新抛出,全局兜底 | catch / unhandledrejection |
| async/await | 让 Promise 代码更像同步写法 | async await + try…catch |
| 微任务 | Promise 回调在微任务队列,优先级高于 setTimeout | queueMicrotask() |
不要为了用 Promise 而用 Promise。对于简单的异步操作,适当使用回调也完全可以。但当逻辑变复杂(多层嵌套、并发控制、错误处理),Promise 和 async/await 才是合适的工具。
参考来源
- MDN Web Docs: Promise
- JavaScript.info: Promise
- JavaScript.info: Promise Chaining
- JavaScript.info: Error handling with promises
- JavaScript.info: Promise API
- JavaScript.info: Async/await
- Promises/A+ 规范
- web.dev: JavaScript Promises: an introduction
回调地狱(Callback Hell)指嵌套多层回调函数导致代码难以阅读和维护的现象。Promise 通过链式调用解决了这个问题。
↩Promise 状态是单向且不可逆的:pending → fulfilled 或 pending → rejected。一旦状态改变(settled),就永久锁定,后续的 resolve/reject 调用会被忽略。
↩Promise 构造函数的 executor 函数由 JavaScript 引擎立即同步执行。异步操作应在 executor 内部启动,通过 resolve/reject 改变 Promise 状态。
↩Thenable 是指拥有 .then() 方法的对象。Promise.resolve() 可以将 thenable 转换为标准 Promise,这是 Promise 互操作性的基础。
↩Promise 链式调用的关键:每个 .then() 返回一个新 Promise。如果回调返回值,该值被包装为 fulfilled Promise;如果返回 Promise,则等待其完成。
↩Promise 的 .then/.catch/.finally 回调进入微任务队列(Microtask Queue),优先级高于宏任务(setTimeout/setInterval)。当前宏任务完成后,会先清空微任务队列。
↩Promise.all() 并行执行多个 Promise,全部成功才成功,任何一个失败立即失败。返回结果数组顺序与输入顺序一致,而非完成顺序。
↩
评论
GitHub 登录后可评论。
评论区会在滚动到这里时自动加载。