Promise 的本质:状态机
Promise 不是"异步任务本身"。更准确地说:Promise 是 JavaScript 语言层面对异步结果的一个状态机对象。
Promise 有三种状态:
- pending:初始状态,既不是 fulfilled 也不是 rejected
- fulfilled:操作成功完成,Promise 的值变得可用
- rejected:操作失败,Promise 带有一个错误原因
状态转换是单向的、不可逆的——一个 Promise 一旦从 pending 变成 fulfilled 或 rejected,就再也不会改变。
Promise 不是任务,而是代理
理解 Promise 的一个关键点是:Promise 不是异步任务本身,而是异步任务结果的代理。
真正发起 I/O 操作的,是宿主环境(浏览器或 Node.js)。Promise 只是"承诺"这个 I/O 操作最终会有一个结果,然后提供一种统一的方式来处理这个结果。
// fetch 发起网络请求(真正的 I/O 操作)
const promise = fetch('/api/user');
// Promise 不是网络请求本身
// Promise 是对这个请求结果的代理
promise
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
Promise 的单次决议特性
Promise 是一个单次决议的状态机。一旦 resolved 或 rejected,后续的 .then() 调用会立即执行(作为微任务),而不是重新触发异步操作。
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('value'), 1000);
});
promise.then(v => console.log('first then:', v));
promise.then(v => console.log('second then:', v));
// 1 秒后输出:
// first then: value
// second then: value
两个 .then() 都接收到同一个值,因为 Promise resolved 后状态不可改变。
Promise.then() 的本质:注册 Reaction
.then() 的本质不是"取值",而是注册 reaction。
Reaction 是一个函数,当 Promise 的状态变为 fulfilled 或 rejected 时被调用。
const promise = Promise.resolve(42);
// .then() 注册了一个 reaction 函数
// 这个函数在 Promise resolved 后作为微任务执行
promise.then(value => {
console.log('reaction:', value);
});
当多个 .then() 注册在同一个 Promise 上时,它们形成了一个 reaction 链:
Promise.resolve(1)
.then(x => x * 2) // 注册第一个 reaction
.then(x => x + 1) // 注册第二个 reaction
.then(x => console.log(x)); // 注册第三个 reaction
// 输出: 3 (1*2+1)
Promise Chain 的执行机制
Promise chain 是通过 reaction 连接起来的 future graph。理解它的执行机制,对写出正确的异步代码至关重要。
then 返回新的 Promise
每个 .then() 返回一个新的 Promise。这个新 Promise 的状态取决于 reaction 函数的返回值:
- 如果 reaction 返回一个值,新 Promise resolved 为这个值
- 如果 reaction 抛出异常,新 Promise rejected 为这个异常
- 如果 reaction 返回一个 Promise,新 Promise 会"跟随"那个 Promise
Promise.resolve(1)
.then(x => {
console.log('first:', x); // 1
return x + 1;
})
.then(x => {
console.log('second:', x); // 2
return Promise.reject('error');
})
.then(x => {
console.log('third:', x); // 不执行
})
.catch(err => {
console.log('catch:', err); // 'error'
})
.finally(() => {
console.log('finally'); // 执行
});
微任务的时序
Promise reaction 是在微任务队列中执行的。这意味着:
console.log('sync 1');
Promise.resolve()
.then(() => console.log('microtask'));
console.log('sync 2');
// 输出: sync 1, sync 2, microtask
Promise Resolution Procedure
Promise Resolution Procedure 是 Promise 规范中最复杂的部分之一。它的核心是:Promise 认的是 thenable 协议,是对 thenable 的递归吸收机制。
什么是 Thenable
Thenable 是一个普通对象,只要它有 .then() 方法,就是 thenable:
const thenable = {
then(resolve, reject) {
resolve(42);
}
};
// thenable 被 Promise.resolve() 包装后,会被"吸收"
const promise = Promise.resolve(thenable);
promise.then(value => {
console.log(value); // 42
});
递归吸收机制
Promise.resolve() 如果接收到的值本身是 Promise(或 thenable),它会等待那个 Promise resolved(或 rejected),然后返回一个新的 Promise:
const p1 = new Promise(resolve => setTimeout(() => resolve(1), 100));
const p2 = Promise.resolve(p1); // p2 跟随 p1
p2.then(value => console.log(value)); // 1,100ms 后输出
常见陷阱
陷阱一:忘记了 Promise 是异步的
function getData() {
const promise = fetch('/api/data');
return promise; // 正确:返回 Promise 对象
}
async function wrong() {
const data = getData();
console.log(data); // Promise 对象,不是数据
}
async function right() {
const data = await getData();
console.log(data); // 真实数据
}
陷阱二:忘记 catch promise rejection
// 没有处理 rejection 的 Promise 不会被 GC
// 在 Node.js 中会产生 UnhandledPromiseRejectionWarning
someAsyncOperation()
.then(result => process(result));
// 正确做法:始终处理 rejection
someAsyncOperation()
.then(result => process(result))
.catch(error => console.error(error));
async/await 与 Promise 的关系
async/await 是 Promise 的语法糖,不是新的异步机制。
async 函数的返回值
async 函数总是返回一个 Promise。如果 async 函数正常返回,其 Promise resolved 为返回值;如果抛出异常,Promise rejected 为异常。
async function foo() {
return 42;
}
foo().then(value => console.log(value)); // 42
await 的等待机制
await 等待的是一个 Promise。await 暂停当前 async 函数的执行,将函数中 await 之后的代码作为微任务加入队列。
async function foo() {
console.log('a');
const value = await Promise.resolve(1);
console.log('b'); // 微任务执行
return value + 1;
}
Promise 的工程实践
Promise.all:并行等待
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
Promise.all 等待所有 Promise resolved,如果任意一个 rejected,整体立即 rejected。
Promise.race:竞态
// 设置超时
const withTimeout = (promise, timeout) =>
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), timeout)
)
]);
Promise.allSettled:全部完成
const results = await Promise.allSettled([
fetch('/api/first').then(r => r.json()),
fetch('/api/second').then(r => r.json())
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`API ${i}:`, result.value);
} else {
console.log(`API ${i} failed:`, result.reason);
}
});
这一章想说的
Promise 是 JavaScript 异步编程的核心。它的本质是:
- 状态机:pending → fulfilled/rejected,不可逆
- 结果代理:不是异步任务本身,而是任务结果的代理
- Reaction 注册:
.then()注册回调,不是立即执行 - 微任务执行:reaction 在微任务队列中执行
- 递归吸收:Promise.resolve 遇到 thenable 会递归处理
async/await 是 Promise 的语法糖,理解 Promise 的执行模型,就理解了 async/await。
延展阅读
- MDN: Promise — Promise 的完整 API 文档
- Promise 规范解读 — Promise/A+ 规范官网
- Dan Abramov: Untangling Spaghetti Code — 关于异步代码组织的思考
实践练习
练习一:实现一个 Promise.retry
function fetchWithRetry(url, options = {}, retries = 3) {
return fetch(url, options)
.catch(err => {
if (retries <= 0) throw err;
return new Promise(resolve =>
setTimeout(() => resolve(), 1000)
).then(() => fetchWithRetry(url, options, retries - 1));
});
}
思考:这个实现有什么问题?应该如何改进?(提示:考虑什么情况下应该重试,什么情况下不应该)
练习二:分析以下代码的执行顺序
function executor(resolve, reject) {
console.log('executor');
resolve();
}
const promise = new Promise(executor);
promise.then(() => console.log('then 1'));
promise.then(() => console.log('then 2'));
console.log('sync');
输出是什么?为什么 then 1 和 then 2 都在 sync 之后输出?