基础进阶:Promise 全链路知识图谱
一、 溯源:Promise 的诞生与进化
在 Promise 普及之前,前端处理异步主要靠回调函数(Callback) 。
- 痛点:当多个异步操作存在依赖关系时,代码会向右不断延伸,形成**“回调地狱”**。这不仅导致阅读困难,更让错误处理(Try-Catch)变得极其碎片化。
- 进化:为了解决代码组织问题,社区先后出现了
Q.js、Bluebird等库,最终 ES6 将其纳入标准。
二、 核心底层:Promise/A+ 规范的实质内容
Promise/A+ 是社区制定的行业标准,它不关心 API 怎么起名,只关心 then 方法 如何交互。
1. 严格的状态机模型
规范规定 Promise 必须处于以下三种状态之一,且迁移关系不可逆:
- Pending:初始态,可迁移至下两者。
- Fulfilled:终态,必须拥有一个不可变的终值(Value)。
- Rejected:终态,必须拥有一个不可变的据因(Reason)。
2. Then 方法的执行准则
- 必须返回新 Promise:
promise2 = promise1.then(onFulfilled, onRejected)。这保证了链式调用的可行性。 - 异步调用:规范明确要求
onFulfilled必须在执行栈仅包含平台代码后执行。
3. 递归解决程序(Resolution Procedure)
这是 A+ 规范最核心的算法:[[Resolve]](promise, x)。 当你在 then 中返回一个值 x 时:如果 x 是一个 Thenable(带有 then 方法的对象),Promise 会递归地“剥开”它,直到取到最底层的原始值。
三、 执行节奏:宏任务与微任务
理解了规范中的“异步执行”后,就需要引入浏览器的事件循环(Event Loop) 。
- 宏任务 (MacroTask) :宿主环境发起。如
setTimeout,setInterval,I/O,script整体代码。 - 微任务 (MicroTask) :JS 引擎自身发起。如
Promise.then,Async/Await,MutationObserver。
执行公式:
- 执行一个宏任务。
- 宏任务执行完,立即清空整个微任务队列。
- 浏览器进行渲染更新。
- 取下一个宏任务。
四、 面试演练:由简入难的消解
1. 基础启动顺序
JavaScript
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
- 结果:
1, 4, 3, 2(同步 > 微任务 > 宏任务)。
2. Async/Await 拆解
JavaScript
async function async1() {
console.log('A');
await async2(); // 之后的内容被放入微任务
console.log('B');
}
async function async2() { console.log('C'); }
console.log('D');
async1();
console.log('E');
- 结果:
D, A, C, E, B(await 之前同步执行,之后异步)。
3. 混合嵌套(极限挑战)
JavaScript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
setTimeout(() => console.log('6'), 0);
});
console.log('7');
- 推演:同步
1, 4, 7-> 微任务5-> 宏任务2-> 产生微任务3-> 宏任务6。 - 结果:
1, 4, 7, 5, 2, 3, 6。
五、 手撕 Promise:核心实现参考
实现一个符合 A+ 规范的 Promise,重点在于状态转换和 then 的异步收集。
JavaScript
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.onResolvedCallbacks = []; // 存储成功回调
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn()); // 发布
}
};
try { executor(resolve, (r) => {}); } catch (e) { /* reject */ }
}
then(onFulfilled) {
// 简单实现:如果是异步,先订阅
if (this.status === 'pending') {
this.onResolvedCallbacks.push(() => onFulfilled(this.value));
}
if (this.status === 'fulfilled') onFulfilled(this.value);
return this; // 链式支持
}
}
六、 并发与竞赛:高级 API 详解
在实际业务中,我们经常需要同时处理多个异步任务。ES 规范提供了四种主要的并发处理方法:
1. 并发控制 API 对比
| 方法 | 成功机制 | 失败机制 | 典型应用场景 |
|---|---|---|---|
Promise.all | 全部成功才成功,返回结果数组。 | 一个失败即失败(中断),抛出该错误。 | 多个接口同时加载,缺一不可(如详情页初始化)。 |
Promise.race | 第一个完成的(无论成败)即为最终状态。 | 同左。 | 接口超时控制、资源竞速。 |
Promise.allSettled | 全部执行完毕(无论成败),返回状态对象数组。 | 永远不会进入 catch(除非参数不可迭代)。 | 统计一组任务的执行结果,不关心成败。 |
Promise.any | 一个成功即成功。 | 全部失败才失败,抛出 AggregateError。 | 多源加载(如从三个 CDN 取同一个资源,只要一个通了就行)。 |
七、 扩展与补丁:Promise.try
1. 为什么需要 Promise.try?
在处理可能抛出同步错误的函数时,我们希望统一用 .catch() 来捕获。
JavaScript
// 如果 f() 是同步报错,这里的 catch 捕获不到
try {
f();
} catch (e) { ... }
// 使用 Promise.try (目前是提案阶段,常用蓝鸟库或手动实现)
Promise.try(() => f()).then(...).catch(...)
2. 手写实现
JavaScript
Promise.try = function(callback) {
return new Promise((resolve) => {
resolve(callback()); // 即使 callback 报错,也会被 Promise 内部捕获并转为 reject
});
};
八、 核心手写:Promise.all & Race
这是面试中最常考察的底层逻辑实现。
1. Promise.all 实现要点
- 返回一个新 Promise。
- 维护一个结果数组
results和一个计数器count。 - 只有当
count === length时才resolve。
JavaScript
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
let results = [];
let count = 0;
promises.forEach((p, index) => {
// 包装成 Promise 以处理非 Promise 元素
Promise.resolve(p).then(res => {
results[index] = res; // 保证顺序
count++;
if (count === promises.length) resolve(results);
}).catch(reject); // 一个失败,直接 reject
});
});
};
2. Promise.race 实现要点
- 利用 Promise 状态一旦改变就不可逆的特性。
JavaScript
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach(p => {
Promise.resolve(p).then(resolve, reject);
});
});
};
九、 实战进阶:并发量限制 (Limit)
面试官可能会追问:“如果我有 100 个请求,但浏览器限制并发数为 5,该怎么办?”
思路:维护一个执行池,每当一个任务完成,就从等待队列中取出一个新任务补充进去。
JavaScript
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);
}