什么是Promise 原理
Promise 原理 可以概括为以下几个核心要点:
-
状态机
Promise 是一个拥有三种状态的对象:pending(进行中)、fulfilled(已成功)、rejected(已失败)。- 状态只能由
pending变为fulfilled或rejected,且一旦改变就不可逆(保证结果稳定)。
- 状态只能由
-
回调注册与异步执行
通过.then()方法注册成功和失败的回调函数。- 如果 Promise 已经处于最终状态,回调会被异步执行(微任务)。
- 如果还在
pending,回调会被存入队列,待状态变更后依次调用。
-
链式调用
.then()总是返回一个新的 Promise,从而支持链式操作。- 上一个回调的返回值会传递给下一个
.then()。 - 如果返回值是一个 Promise(或 thenable 对象),则会等待它解析完毕再继续。
- 上一个回调的返回值会传递给下一个
-
值的穿透与错误处理
- 如果
.then()缺少对应回调,值会直接穿透到下一个链节。 - 错误可以通过
.catch()统一捕获,类似于同步代码的try/catch。
- 如果
-
符合 Promise/A+ 规范
规范定义了then方法的行为细节,包括如何解析 thenable 对象、防止循环引用、保证异步调用等,确保所有 Promise 实现可以互操作。
简而言之,Promise 是一种异步编程的解决方案,它通过状态机管理异步操作的结果,并利用回调队列和链式调用来组织异步代码,使其更接近同步编程的写法,避免了“回调地狱”。
什么是并发限制
并发限制是指在同一时间内,允许同时执行的任务或操作的最大数量。
在异步编程中,如果不加限制地同时发起大量请求(如网络请求、文件读写、数据库操作),可能会耗尽系统资源(如内存、网络连接)、导致服务响应变慢甚至崩溃。因此,通过设置并发限制,可以控制同时运行的任务数,确保系统平稳运行。
举例:
- 浏览器通常限制同一域名下的并发请求数为 6 个,这就是一种并发限制。
- 在 Node.js 中,如果需要读取 100 个文件,可以设置一次最多读 5 个,等某个读完再读下一个,避免文件描述符耗尽。
并发限制通常通过 任务队列 实现:将待执行的任务放入队列,维护当前正在执行的任务计数;每当一个任务完成,就从队列中取出下一个任务执行,确保同时执行的任务数不超过设定的上限。
一个形象比喻
用一个更精简的 “奶茶店” 比喻:
- Promise 就像一张 取餐小票,它代表一个未来的结果(奶茶),状态只有“待制作”、“已完成”或“已取消”,一旦确定就不会再变。
- 异步控制 好比奶茶店只有 2 个制作员(并发限制),同时最多做两杯奶茶。
- 队列思想 就是 排队系统:多出来的订单先进先出地等待,每当一个制作员空闲,就从队首取出下一个订单开始做。
这个比喻概括了 Promise 的状态管理、并发控制和任务排队三个核心概念,简洁而形象。
手写符合 Promise/A+ 规范的 Promise
javascript
function MyPromise(executor) {
let self = this;
self.status = 'pending';
self.value = undefined;
self.reason = undefined;
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
function resolve(value) {
// 处理 thenable 对象(包括原生 Promise 或其他符合 then 方法的对象)
if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
let then;
try {
then = value.then;
} catch (err) {
reject(err);
return;
}
if (typeof then === 'function') {
// 递归解析 thenable,直到得到一个非 thenable 的值或最终状态
then.call(value, resolve, reject);
return;
}
}
// 确保状态改变是异步的(使用 setTimeout 模拟微任务,实际应为微任务)
setTimeout(() => {
if (self.status === 'pending') {
self.status = 'fulfilled';
self.value = value;
self.onFulfilledCallbacks.forEach(cb => cb(value));
}
});
}
function reject(reason) {
setTimeout(() => {
if (self.status === 'pending') {
self.status = 'rejected';
self.reason = reason;
self.onRejectedCallbacks.forEach(cb => cb(reason));
}
});
}
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
let self = this;
let promise2;
// 值穿透:如果回调不是函数,则忽略并传递当前值
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
if (self.status === 'fulfilled') {
promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
});
}
if (self.status === 'rejected') {
promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
});
}
if (self.status === 'pending') {
promise2 = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
self.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
});
}
return promise2;
};
// 核心的解析函数:处理 then 回调返回的值 x,决定 promise2 的状态
function resolvePromise(promise2, x, resolve, reject) {
// 防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called = false; // 确保只能调用一次
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
// x 是一个 thenable,将其作为 promise 处理
then.call(
x,
y => {
if (called) return;
called = true;
// 递归解析,直到 y 不是 thenable
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
// x 是一个普通对象
resolve(x);
}
} catch (err) {
if (called) return;
called = true;
reject(err);
}
} else {
// x 是基本类型值
resolve(x);
}
}
// 添加 catch 方法
MyPromise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
};
// 静态 resolve 方法
MyPromise.resolve = function(value) {
return new MyPromise(resolve => resolve(value));
};
// 静态 reject 方法
MyPromise.reject = function(reason) {
return new MyPromise((_, reject) => reject(reason));
};
说明
-
处理 thenable 对象(包括原生 Promise)
- 在
resolve方法中,如果传入的值是对象或函数,并且拥有then方法,则将其视为 thenable 并递归解析,直到得到最终值或最终状态。这符合 Promise/A+ 规范对resolve的处理要求。 - 之前版本只检查了
value instanceof MyPromise,现在通过检查then方法的存在来兼容任何符合 Promise/A+ 的 thenable(如原生 Promise、其他库的 Promise)。
- 在
-
状态变更的异步性
- 使用
setTimeout模拟异步,确保then中的回调总是在当前代码执行完毕后调用(实际应为微任务,这里简化)。关键是在状态变更时,需要异步执行回调,保证then总是异步的。
- 使用
-
resolvePromise函数的健壮性- 严格处理循环引用:如果
promise2和x是同一个对象,抛出TypeError。 - 使用
called标志防止多次调用(例如,当 thenable 同时调用resolve和reject,或多次调用同一个回调)。 - 递归解析:当 x 是 thenable 时,继续调用
resolvePromise解析其返回值,直到得到非 thenable 值。
- 严格处理循环引用:如果
-
值穿透
- 在
then方法中,如果传入的onFulfilled或onRejected不是函数,则用默认函数代替,使得值能够穿透到下一个then或catch。
- 在
-
错误捕获
- 在
executor执行时和回调执行时都使用try/catch捕获同步错误,并调用reject。
- 在
并发请求调度器
javascript
class Scheduler {
constructor(limit) {
this.limit = limit; // 最大并发数
this.queue = []; // 等待队列,存放 { task, resolve, reject }
this.runningCount = 0; // 当前正在执行的任务数
}
// 添加一个任务,返回一个 Promise,该 Promise 在任务实际完成时 resolve/reject
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.next(); // 尝试执行下一个任务
});
}
// 尝试从队列中取出任务执行(如果并发未满)
next() {
if (this.runningCount < this.limit && this.queue.length) {
const { task, resolve, reject } = this.queue.shift();
this.runningCount++;
// 使用 Promise.resolve().then 确保 task() 的同步错误能被捕获
Promise.resolve()
.then(() => task())
.then(result => resolve(result))
.catch(error => reject(error))
.finally(() => {
this.runningCount--;
this.next(); // 任务完成,继续尝试下一个
});
}
}
}
说明
-
使用
Promise.resolve().then()包裹任务执行- 原代码直接调用
task(),如果task同步抛出异常(例如非函数调用错误),会导致程序崩溃。现在通过Promise.resolve().then(() => task())将同步错误转化为 Promise 的 rejected 状态,能够被catch捕获并传递给外层的reject。
- 原代码直接调用
-
任务队列的封装
- 每个添加的任务被包装成一个包含
task函数和外部resolve/reject的对象。当任务实际完成时,调用对应的resolve/reject,使外部能通过add返回的 Promise 获取结果。
- 每个添加的任务被包装成一个包含
-
并发控制逻辑
runningCount记录当前执行数,小于限制时从队列头部取出任务执行。- 任务执行完毕(无论成功或失败)后,
runningCount减一,并调用next()尝试启动下一个任务。 - 使用
finally保证无论任务成功还是失败,计数都会减少,队列继续推进。
-
支持任意返回 Promise 的函数
task可以是任意返回 Promise 的函数,调度器不关心任务内部实现,只负责并发控制和结果传递。
总结
- 手写 Promise 实现了 Promise/A+ 规范的核心:状态机、then 链式调用、thenable 解析、值穿透、错误处理。
- 并发调度器展示了异步队列管理的经典模式:维护等待队列和并发计数,通过递归调用
next推动任务执行,并安全捕获同步错误。
#前端、#前端面试、#干货
如果这篇这篇文章对您有帮助?需要你的三连 关注、点赞、收藏,谢谢。
有疑问或想法?评论区见。