技术备忘 JavaScriptPromise异步编程

前言

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(已定型)。

js
const promise = new Promise((resolve, reject) => {
// 异步操作...
if (/* 成功 */) {
resolve(value);
} else {
reject(error);
}
});
NOTE

Promise 的状态只能改变一次,一旦从 pending 变为 fulfilled 或 rejected,就永久锁定。后续的 resolve 或 reject 调用都会被忽略。

创建 Promise

Promise 构造函数

Promise 构造函数接受一个执行器(executor)函数3Promise 构造函数的 executor 函数由 JavaScript 引擎立即同步执行。异步操作应在 executor 内部启动,通过 resolve/reject 改变 Promise 状态。作为参数。执行器函数由 JavaScript 引擎同步执行,负责启动异步操作。

js
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('完成!');
}, 1000);
});

执行器函数接收两个参数:

  • resolve(value):操作成功时调用,将 Promise 状态改为 fulfilled。
  • reject(error):操作失败时调用,将 Promise 状态改为 rejected。
js
const promise = new Promise((resolve, reject) => {
resolve('第一次');
resolve('第二次'); // 忽略
reject(new Error('错误')); // 忽略
});
NOTE

建议在 reject 时总是传入 Error 对象(或其子类),而非普通字符串 —— 这样能得到完整的调用栈信息,便于调试。

Promise.resolve() 与 Promise.reject()

快速创建已定型 Promise:

js
const resolved = Promise.resolve(42);
const rejected = Promise.reject(new Error('失败'));

Promise.resolve 尤其适合在函数需要统一返回 Promise 时使用,也可以将 thenable4Thenable 是指拥有 .then() 方法的对象。Promise.resolve() 可以将 thenable 转换为标准 Promise,这是 Promise 互操作性的基础。 转换为标准 Promise,比如缓存场景:

js
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 最核心的方法,它接受两个回调参数:

js
promise.then(
(result) => { /* 处理成功 */ },
(error) => { /* 处理失败 */ }
);
js
const promise = new Promise((resolve) => {
setTimeout(() => resolve('done!'), 1000);
});
promise.then(
result => console.log(result), // 1秒后输出 "done!"
error => console.log(error) // 不会执行
);

如果只关心成功结果,可以只传一个参数:

js
promise.then(console.log);

catch()

.catch(f) 等价于 .then(null, f),专门用来处理错误:

js
const promise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Whoops!')), 1000);
});
promise.catch(error => console.log(error.message)); // "Whoops!"

finally()

.finally(f) 不论 Promise 成功还是失败都会执行,适合做清理工作(如关闭加载动画、释放资源):

js
showLoading();
fetch(url)
.then(response => response.json())
.catch(error => console.error(error))
.finally(() => hideLoading());

.finally 的特殊之处:

特性说明
无参数不知道 Promise 结果,只用于清理
透传结果将 value 或 error 原样交给下一级
不可改变状态除非 finally 内部抛出异常
NOTE

.finally(f) 不等同于 .then(f, f):前者不接收参数且结果会透传,后者会接收结果值并可能改变链式传递的值。

链式调用(Promise Chaining)5Promise 链式调用的关键:每个 .then() 返回一个新 Promise。如果回调返回值,该值被包装为 fulfilled Promise;如果返回 Promise,则等待其完成。

链式调用是 Promise 最大的亮点之一。每次调用 .then() 都会返回一个新的 Promise,因此可以无限链式调用下去。

js
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 上
结果传递上游结果 → 下游各自独立接收相同值
实际用途串联异步操作并联多个独立回调
js
const p = new Promise(resolve => resolve(1));
// ✅ 链式 — 值层层传递
p.then(r => r * 2).then(r => console.log(r)); // 2
// ❌ 分支 — 三个处理器各自独立,都得到 1
p.then(r => console.log(r)); // 1
p.then(r => console.log(r)); // 1
p.then(r => console.log(r)); // 1

返回 Promise 的链式调用

.then() 中返回一个 Promise,下一个 .then() 会等待它完成:

js
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 结合的实际例子

js
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}`));
NOTE

一个良好的实践:异步操作应该始终返回 Promise。这样即使现在不需要扩展链,将来也可以轻松地在后面添加更多操作。上面的例子通过返回一个 Promise 来控制下一级 .then() 的执行时机。

错误处理

隐式 try…catch

Promise 的执行器和处理器内部有一个隐式 try…catch。Promise 回调进入微任务队列6Promise 的 .then/.catch/.finally 回调进入微任务队列(Microtask Queue),优先级高于宏任务(setTimeout/setInterval)。当前宏任务完成后,会先清空微任务队列。。如果抛出异常,会自动转为 rejected Promise:

js
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() 处理器:

js
new Promise((resolve, reject) => {
resolve('ok');
})
.then(result => {
throw new Error('then 中出错');
})
.catch(console.log); // Error: then 中出错

错误传播

在链式调用中,只要有一个 Promise 被 rejected,控制权就跳转到最近的 rejection 处理器:

js
fetch('https://no-such-server.example.com')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('捕获错误:', error));

重新抛出(Rethrowing)

.catch() 内部,如果错误无法处理,可以重新抛出,交给链中更下游的 .catch()

js
new Promise((resolve, reject) => {
throw new Error('Whoops!');
})
.catch(error => {
if (error instanceof URIError) {
// 处理特定类型错误
} else {
console.log('无法处理此类错误');
throw error; // 重新抛出
}
})
.catch(error => {
console.log(`未知错误: ${error}`);
});
NOTE

好的错误处理习惯:在 .catch()识别已知错误,重新抛出未知错误。可以让链末端的最后一个 .catch() 处理非预期的异常,或者交给全局错误处理器。

未捕获的拒绝(Unhandled Rejection)

如果 Promise 被 rejected 却没有对应的 .catch() 处理,JavaScript 引擎会触发全局事件:

js
window.addEventListener('unhandledrejection', (event) => {
console.log(`未处理的 Promise 错误: ${event.reason}`);
// 可以在此上报错误到服务器
});

在 Node.js 中对应的是 unhandledRejection 事件。

Promise 静态方法

Promise 类提供 6 个静态方法,下面逐一详解。

Promise.all()7Promise.all() 并行执行多个 Promise,全部成功才成功,任何一个失败立即失败。返回结果数组顺序与输入顺序一致,而非完成顺序。

并行执行多个 Promise,等待全部完成。如果任何一个被 rejected,整体立即失败。

js
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 更慢:

js
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 — 保持输入顺序
NOTE

如果一个 Promise 被 rejected,其他 Promise 不会自动取消,它们仍然会继续执行,只是结果被忽略。这在一些场景下可能导致意外的并发行为(见后面常见误区)。

Promise.all 也接受非 Promise 值,它们会”原样”通过:

js
Promise.all([
new Promise(r => setTimeout(() => r(1), 1000)),
2,
3,
]).then(console.log); // [1, 2, 3]

Promise.allSettled()

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

js
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);
}
});
});

返回格式:

json
[
{ "status": "fulfilled", "value": ... },
{ "status": "rejected", "reason": ... }
]

如果浏览器不支持 Promise.allSettled,可以使用以下 polyfill:

js
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,以它的结果为准(无论成功还是失败):

js
Promise.race([
fetch('/api/fast'),
fetch('/api/slow'),
]).then(response => {
console.log('最快的请求返回了');
});

Promise.any()

等待第一个 fulfilled 的 Promise。如果全部被 rejected,则返回 AggregateError

js
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 的 Promisenew Promise(r => r(value))
Promise.reject(error)创建一个 rejected 的 Promisenew Promise((_, r) => r(error))

Thenable(可 then 对象)

Promise 的互操作性基于 .then() 方法。任何实现了 .then() 方法的对象都被称为 thenable,可以被 Promise 的处理机制识别:

js
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
NOTE

这意味着第三方库可以实现自己的”Promise 兼容”对象,而无须继承 Promise。只要对象有 .then() 方法,就能与 JavaScript 原生的 Promise 机制无缝协作。

async / await

async/await 是 Promise 的语法糖,让异步代码看起来像同步代码。

async 关键字

async 声明的函数总是返回一个 Promise:

js
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 函数中使用:

js
async function getUserData() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
js
// ❌ SyntaxError:不能在普通函数中使用 await
function f() {
let result = await Promise.resolve(1); // 报错
}

改用 async/await 重写 fetch 链式调用

将前面的链式示例改为 async/await:

js
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 捕获错误:

js
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 传播:

js
async function loadUser() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
loadUser().catch(error => console.error(error));
NOTE

async/awaittry…catch 的组合比 .then().catch() 链式更加直观,尤其在需要处理多个 await 表达式的错误时,可以用一个 try 块包裹全部。

顶层 await

在 ES2022 中,模块的顶层可以直接使用 await

js
// 仅在模块中有效
const config = await fetch('/config.json').then(r => r.json());
console.log(config);

如果不是模块环境,可以使用 IIFE(立即执行函数表达式)来模拟:

js
(async () => {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
})();

await 与 Promise.all

并行等待多个 Promise 的正确方式:

js
// ❌ 错误:串行执行(一个等一个)
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 化

js
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)

js
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
),
]);
}

重试机制

js
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} 次...`);
}
}
}

并发控制

js
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

js
// ❌ 错误:没有 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 判断状态

js
const p = Promise.resolve(42);
console.log(p.state); // ❌ 无法访问内部状态
console.log(p.result); // ❌ 无法直接访问结果

Promise 的 stateresult 是内部属性,无法直接从外部读取,只能通过 .then() / .catch() 获取。

误区 3:错误地被 finally 吞掉

js
// ❌ 如果 finally 内部抛出错误,会覆盖原有错误
Promise.reject(new Error('原始错误'))
.finally(() => { throw new Error('finally 错误'); })
.catch(error => console.log(error.message)); // "finally 错误"

误区 4:回调写法嵌套(Pyramid of Doom)

js
// ❌ 错误:仍然嵌套,退化为回调地狱
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 中一个失败其他不会取消

js
// 即使一个请求失败,其他请求仍然在飞
Promise.all([
fetch('/api/a'),
fetch('/api/b'), // 这个会失败
fetch('/api/c'), // 这个仍然会发送并执行,但其结果/错误被忽略
]).catch(error => {
console.error(error);
});

误区 6:then 的第二个参数 vs catch

js
// ⚠️ 区别:promise.then(f1, f2) vs promise.then(f1).catch(f2)
// 前者不会捕获 f1 内部的错误,后者会!
promise.then(f1, f2); // f1 抛出错误 → 不会进入 f2
promise.then(f1).catch(f2); // f1 抛出错误 → 进入 f2

高级话题:微任务队列

Promise 的处理基于 JavaScript 的事件循环(Event Loop)中的微任务队列(Microtask Queue)

队列优先级示例
宏任务(Macrotask)setTimeoutsetInterval、IO
微任务(Microtask)Promise .then()queueMicrotask()
js
setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
console.log('同步代码');
// 输出顺序:
// 同步代码 → 微任务 → 宏任务
NOTE

理解微任务队列对调试异步代码非常关键。Promise 的回调总是在当前宏任务结束、下一个宏任务开始之前执行,这解释了为什么 Promise.resolve().then() 的优先级高于 setTimeout(fn, 0)

浏览器兼容性与 Polyfill

特性ES 版本支持范围
Promise 基础ES2015所有现代浏览器
Promise.all / raceES2015所有现代浏览器
Promise.allSettledES2020现代浏览器,需 polyfill 较旧环境
Promise.anyES2021现代浏览器,需 polyfill 较旧环境
AggregateErrorES2021与 Promise.any 同时支持
async/awaitES2017所有现代浏览器
顶层 awaitES2022现代浏览器的模块上下文

如需兼容老旧浏览器,可使用 core-js 等 polyfill:

bash
npm install core-js
js
import 'core-js/actual/promise';
import 'core-js/actual/promise/all-settled';

总结

JavaScript Promise 是处理异步编程的核心机制,它解决了回调地狱的问题。通过这张表可以快速回顾全文要点:

知识点要点常用方法
Promise 基础三个状态:pending / fulfilled / rejectednew 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 回调在微任务队列,优先级高于 setTimeoutqueueMicrotask()
NOTE

不要为了用 Promise 而用 Promise。对于简单的异步操作,适当使用回调也完全可以。但当逻辑变复杂(多层嵌套、并发控制、错误处理),Promise 和 async/await 才是合适的工具。


参考来源

脚注
  1. 回调地狱(Callback Hell)指嵌套多层回调函数导致代码难以阅读和维护的现象。Promise 通过链式调用解决了这个问题。

  2. Promise 状态是单向且不可逆的:pending → fulfilled 或 pending → rejected。一旦状态改变(settled),就永久锁定,后续的 resolve/reject 调用会被忽略。

  3. Promise 构造函数的 executor 函数由 JavaScript 引擎立即同步执行。异步操作应在 executor 内部启动,通过 resolve/reject 改变 Promise 状态。

  4. Thenable 是指拥有 .then() 方法的对象。Promise.resolve() 可以将 thenable 转换为标准 Promise,这是 Promise 互操作性的基础。

  5. Promise 链式调用的关键:每个 .then() 返回一个新 Promise。如果回调返回值,该值被包装为 fulfilled Promise;如果返回 Promise,则等待其完成。

  6. Promise 的 .then/.catch/.finally 回调进入微任务队列(Microtask Queue),优先级高于宏任务(setTimeout/setInterval)。当前宏任务完成后,会先清空微任务队列。

  7. Promise.all() 并行执行多个 Promise,全部成功才成功,任何一个失败立即失败。返回结果数组顺序与输入顺序一致,而非完成顺序。