Promise 最难的部分不是记住 then 的语法,而是看清"哪个 Promise 已经确定结果""错误会沿哪一段链传播""回调何时执行"。掌握这三件事,就能读懂链式调用、Promise.all 和 async/await。本文示例均可在现代浏览器控制台或 Node.js 中运行。
一 从基础开始 Promise 代表什么
Promise 是一个异步操作结果的占位对象,有三种状态:pending(待定)、fulfilled(成功)和 rejected(失败)。成功保存一个值,失败保存一个原因。
javascript
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("数据已返回"), 100);
});
p.then(
value => console.log(value),
reason => console.error(reason)
);
构造器里的执行器函数会立即同步执行 ;then 注册的处理函数会在相应结果确定后进入微任务队列,不会在当前同步代码中直接执行。
二 三条必须记住的规则
规则一 状态只能确定一次
Promise 一旦从 pending 走向 fulfilled 或 rejected,后续对它的 resolve、reject 调用不再改变结果。
javascript
const p = new Promise((resolve, reject) => {
resolve("第一次");
reject(new Error("太晚了"));
resolve("第二次");
});
p.then(console.log); // 第一次
更精确地说,调用 resolve(另一个 Promise) 时,外层 Promise 已经"锁定"到那个 Promise 的最终结果;它此时仍可能显示为 pending,但后续 reject 也无法抢先改写结果。
javascript
let finish;
const inner = new Promise(resolve => { finish = resolve; });
const outer = new Promise((resolve, reject) => {
resolve(inner); // 结果锁定到 inner
reject(new Error("无效")); // 不会生效
});
outer.then(console.log);
finish("最终值"); // 最终值
规则二 throw 会把当前链节变成失败
执行器或 then 回调中抛出的异常,会使对应的新 Promise 变为 rejected。但异步定时器中抛出的异常,不在构造器同步捕获范围内,不能靠外层 Promise 自动接住。
typescript
Promise.resolve(1)
.then(() => { throw new Error("计算失败"); })
.catch(error => console.log(error.message)); // 计算失败
throw new Error(...) 与在 then 回调中 return Promise.reject(new Error(...)),都能让这一步返回的 Promise 失败。推荐用 Error 对象保留堆栈信息。
规则三 resolve 会展开嵌套 reject 不会
resolve 接收 Promise 或 thenable 时,会跟随其最终状态和值;reject 则直接把传入内容当作失败原因。
typescript
const inner = Promise.resolve(42);
new Promise(resolve => resolve(inner))
.then(value => console.log(value)); // 42
new Promise((_, reject) => reject(inner))
.catch(reason => console.log(reason === inner)); // true
所以"状态锁定"与"状态已成功"不是一回事:resolve(inner) 可能最终失败,取决于 inner。
三 静态工厂方法 Promise.resolve 与 Promise.reject
Promise.resolve(value) 创建一个最终遵循 value 的 Promise。普通值会成为成功值;Promise 或 thenable 会被展开。传入同一个原生 Promise 时,通常直接返回该 Promise。
javascript
Promise.resolve(7).then(console.log); // 7
Promise.resolve(Promise.resolve(8)).then(console.log); // 8
Promise.reject(reason) 创建失败的 Promise,不会展开 reason;即使原因本身是 Promise,也原样保留。
typescript
const reason = Promise.resolve("内部成功");
Promise.reject(reason)
.catch(error => console.log(error === reason)); // true
这两个方法都立即返回 Promise,但 then / catch 回调仍在后续微任务中执行。"同步返回 Promise"不等于"同步执行回调"。Promise.reject(new Error("请求失败")) 可以构造一个失败结果;实际代码里应及时附上错误处理,避免未处理的拒绝。
四 从回调地狱到可组合的链
假设要依次读取用户、订单和详情。层层嵌套时,每一步的成功与失败分支会不断加深:
scss
getUser(userId, (error, user) => {
if (error) return handleError(error);
getOrders(user.id, (error, orders) => {
if (error) return handleError(error);
getDetail(orders[0].id, (error, detail) => {
if (error) return handleError(error);
render(detail);
});
});
});
把每个 API 包装成返回 Promise 的函数后,顺序流程可以写成一条链:
ini
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getDetail(orders[0].id))
.then(detail => render(detail))
.catch(handleError);
每次 then 都返回一个新的 Promise。回调返回普通值,下一步收到该值;返回 Promise,下一步等待它;抛错或返回失败 Promise,后续成功回调会跳过,直到遇到错误处理。
别漏写 return,否则下一步会过早执行:
ini
getUser(userId)
.then(user => {
return getOrders(user.id); // 把异步结果交给下一步
})
.then(orders => console.log(orders));
五 then 的第二个参数与 catch 的区别
then(onFulfilled, onRejected) 的第二个参数处理的是进入这个 then 时 已有的失败;catch(onRejected) 等价于 then(undefined, onRejected),但放在链尾时能接住前面整段链传下来的失败。
typescript
Promise.resolve("ok")
.then(
() => { throw new Error("成功回调出错"); },
error => console.log("第二参数", error)
)
.catch(error => console.log("链尾 catch", error.message));
// 链尾 catch 成功回调出错
同一个 then 的第二参数接不住第一个参数自己抛出的异常,因为异常发生在这个 then 创建的新 Promise 上。它也接不住自己再次抛出的异常;后续 catch 可以。
错误处理函数若正常返回,链会恢复为成功状态:
typescript
Promise.reject(new Error("网络失败"))
.catch(() => "缓存数据")
.then(value => console.log(value)); // 缓存数据
若处理后仍需让调用方知道失败,应重新 throw 该错误。
六 微任务 宏任务和执行顺序
Promise 的 then / catch / finally 回调属于微任务;setTimeout 回调属于任务队列中的后续任务,常被称为宏任务。当前同步代码先执行完,再清空微任务队列,之后才轮到定时器任务。
javascript
console.log("A");
setTimeout(() => console.log("D"), 0);
Promise.resolve()
.then(() => {
console.log("B");
queueMicrotask(() => console.log("C"));
});
console.log("E");
// A E B C D
Promise.resolve() 本身不等待;它立即返回一个已成功的 Promise,但 then 回调仍异步排队。分析顺序时,先列同步输出,再按入队先后处理微任务,最后处理定时器等后续任务。不同运行环境的事件循环细节可能不同,这个例子在现代浏览器和 Node.js 中结果一致。
七 Promise.all 处理并行任务
Promise.all(iterable) 等待一组 Promise 全部成功,成功结果按输入顺序组成数组,与完成先后无关;只要有一个失败,返回的 Promise 就会失败。普通值也能作为输入。
scss
const [user, settings] = await Promise.all([
fetchUser(),
fetchSettings()
]);
独立任务适合并行启动;存在依赖时,应先等待前一步的结果。Promise.all 的"快速失败"不会自动取消其他已启动任务。若需要取消请求,可按具体 API 使用 AbortController。
javascript
Promise.all([
Promise.resolve("A"),
2,
new Promise(resolve => setTimeout(() => resolve("C"), 10))
]).then(console.log); // ["A", 2, "C"]
空数组 Promise.all([]) 会得到成功值 [],但附加的 then 回调仍是微任务。
八 async await 是 Promise 的另一种写法
async 函数总是返回 Promise;return value 对应成功,throw error 对应失败。await 等待 Promise 的结果:成功时得到值,失败时抛出原因,因而可以用 try/catch 处理。
vbnet
async function loadPage(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
return await getDetail(orders[0].id);
} catch (error) {
console.error("加载失败", error);
throw error; // 交给调用方继续处理
}
}
独立任务不要无意中串行等待:
scss
// 并行启动并等待两项结果
const [profile, notices] = await Promise.all([
getProfile(),
getNotices()
]);
await 之后的代码会在后续微任务中继续执行;它不会阻塞整个 JavaScript 线程。
九 一张表复盘
| 写法 | 结果 |
|---|---|
resolve(普通值) |
成功,值就是该普通值 |
resolve(Promise) |
跟随该 Promise 的最终结果 |
reject(任意原因) |
失败,原因原样保留 |
then 回调返回普通值 |
下一步成功 |
then 回调返回 Promise |
下一步等待它 |
then 回调抛错 |
下一步失败 |
catch 正常返回 |
链恢复为成功 |
catch 再次抛错 |
失败继续向后传播 |
读一段 Promise 代码时,逐步问自己:这一行返回的是哪个新 Promise?它是返回值、等待另一个 Promise,还是抛错?当前同步代码结束后,哪些回调会进入微任务队列?这三个问题比背执行顺序口诀更可靠。