146.《手把手教你实现一个符合 Promise/A+ 规范的 JavaScript Promisee》
文章目录
摘要:
Promise是现代 JavaScript 异步编程的基石。你是否曾好奇过Promise内部是如何工作的?本文将带你从零开始,通过一个完整的手写实现,深入理解Promise的核心原理。我们不仅会实现基本功能,还会探讨状态机、微任务、链式调用等关键概念,最终写出一个符合 Promise/A+ 规范的MyPromise。
引言:为什么我们要手动实现 Promise?
在日常开发中,我们频繁使用 Promise、async/await 来处理异步操作。但大多数时候,我们只是“使用者”,对它的内部机制知之甚少。
手动实现 Promise 有以下几个好处:
- 加深理解: 理解
Promise的状态流转、链式调用、错误处理等机制。 - 面试加分: 这是前端面试中的经典手写题,能体现你对异步编程的掌握程度。
- 提升 Debug 能力: 当
Promise出现问题时,你能更快定位原因。
接下来,就让我们一起揭开 Promise 的神秘面纱!
核心设计:状态机 + 任务队列
Promise 的核心是一个状态机。它有三种状态:
- pending(等待中): 初始状态,既没有被 fulfilled,也没有被 rejected。
- fulfilled(已成功): 意味着操作成功完成。
- rejected(已失败): 意味着操作失败。
关键规则:
- 状态只能从
pending变为fulfilled或rejected。 - 一旦状态改变,就不可逆。
基于此,我们定义 MyPromise 类的基本结构:
class MyPromise {
// 1. 定义初始状态和结果
state = 'pending';
result = null;
handlers = []; // 存储 then 的回调
constructor(execute) {
// 2. 执行传入的执行器函数
const reject = (data) => {
this.changeState('rejected', data);
};
const resolve = (data) => {
this.changeState('fulfilled', data);
};
try {
execute(resolve, reject);
} catch (error) {
reject(error); // 捕获执行器中的同步错误
}
}
// 3. 改变状态(核心方法)
changeState(state, data) {
if (this.state !== 'pending') return; // 状态不可逆
this.state = state;
this.result = data;
this.run(); // 状态改变后,立即执行所有回调
}
}
✅ 解析
state和result分别存储当前状态和结果值。handlers是一个数组,用于存放then方法注册的回调函数(我们稍后会详细设计)。constructor接收一个执行器函数execute,并立即执行它,同时提供resolve和reject函数。changeState是状态变更的唯一入口,确保状态只能从pending变更一次。
实现 then 方法:链式调用的基石
then 方法是 Promise 的灵魂,它允许我们注册成功和失败的回调,并支持链式调用。
then(onFulfilled, onRejected) {
// 返回一个新的 Promise,实现链式调用
return new MyPromise((resolve, reject) => {
// 将回调和 resolve/reject 封装成任务
this.handlers.push({
onFulfilled,
onRejected,
resolve,
reject,
});
// 状态改变后,run 会执行这些任务
this.run();
});
}
✅ 解析
then必须返回一个新的Promise,这样才能支持.then().then()的链式调用。- 我们将
onFulfilled、onRejected以及新Promise的resolve、reject封装成一个“任务”对象,存入handlers队列。 - 调用
this.run()尝试执行这些任务。
执行回调:run 与 runOne 方法
当 Promise 状态改变时,我们需要执行所有已注册的回调。
run() {
if (this.state === 'pending') return;
// 使用 while 循环处理所有 handlers
while (this.handlers.length) {
const { onFulfilled, onRejected, resolve, reject } = this.handlers.shift(); // 注意:这里用 shift 更符合队列逻辑
this.runOne(onFulfilled, onRejected, resolve, reject);
}
}
runOne(onFulfilled, onRejected, resolve, reject) {
// 使用微任务包裹,确保异步执行
queueMicrotask(() => {
// 获取当前状态对应的回调
const callback = this.state === 'fulfilled' ? onFulfilled : onRejected;
// 如果回调不是函数,直接透传结果(实现值穿透)
if (typeof callback !== 'function') {
const settled = this.state === 'fulfilled' ? resolve : reject;
settled(this.result);
return;
}
try {
// 执行回调函数
const data = callback(this.result);
// 处理返回值:如果是 Promise,则链式传递
if (this.isPromise(data)) {
data.then(resolve, reject);
} else {
resolve(data); // 普通值,直接 resolve
}
} catch (error) {
reject(error); // 捕获回调中的错误
}
});
}
✅ 解析
run方法在状态改变后被调用,它会遍历handlers队列,逐个执行runOne。runOne使用queueMicrotask(或setTimeout(fn, 0))将回调放入微任务队列,确保异步执行,符合Promise的事件循环机制。- 值穿透: 如果
onFulfilled或onRejected不是函数(比如传了null),则直接将结果透传给下一个then。 - 处理返回值: 这是
Promise链式调用的关键。如果回调返回的是一个Promise,我们就用它的结果来resolve或reject新的Promise;否则,直接用返回值resolve。
工具方法:isPromise 和 microTask
isPromise(prom) {
if (prom && (typeof prom === 'object' || typeof prom === 'function')) {
return typeof prom.then === 'function'; // 检查是否有 then 方法
}
return false;
}
isPromise 用于判断一个值是否是 Promise(或 thenable 对象),这是实现 Promise.resolve 和处理链式调用的基础。
注意: 你的代码中
microTask使用了setTimeout,这实际上是宏任务。真正的Promise使用的是微任务。在现代浏览器中,应使用queueMicrotask或MutationObserver来模拟微任务。为了更准确,建议替换为:
microTask(callback) {
queueMicrotask(callback);
}
// 或者兼容性更好的实现
// if (queueMicrotask) { ... } else { setTimeout(callback, 0); }
测试我们的 MyPromise
让我们用一个经典的例子来测试:
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
}, 300);
});
// 链式调用
p.then(res => {
console.log('res-1', res); // res-1 成功
return '这是第二个成功'; // 普通值
}).then(res => {
console.log('res-2', res); // res-2 这是第二个成功
});
// 多个 then 监听同一个 Promise
p.then(
res => {
console.log('res-3', res); // res-3 成功
},
err => {
console.log('err', err);
}
);
✅ 预期输出:
res-1 成功
res-3 成功
res-2 这是第二个成功
注意:由于微任务的调度,res-1 和 res-3 的顺序取决于 handlers 的处理顺序(push + shift 是 FIFO 队列)。
完整代码
class MyPromise {
state = 'pending';
result = null;
handlers = [];
constructor(execute) {
const reject = data => {
this.changeState('rejected', data);
};
const resolve = data => {
this.changeState('fulfilled', data);
};
try {
execute(resolve, reject);
} catch (error) {
reject(error);
}
}
changeState(state, data) {
if (this.state !== 'pending') return;
this.state = state;
this.result = data;
this.run();
}
isPromise(prom) {
if (prom && (typeof prom === 'function' || typeof prom === 'object')) {
return prom?.then;
}
return false;
}
microTask(callBack) {
setTimeout(callBack, 0);
}
runCOne(callBack, resolve, reject) {
microTask(() => {
if (typeof CallBack !== 'function') {
const settled = this.state === 'fulfilled' ? resolve : reject;
settled(this.result);
return;
}
try {
const data = callBack(this.result);
if (this.isPromise(data)) {
data.then(resolve, reject);
} else {
resolve(data);
}
} catch (error) {
reject(error);
}
});
}
run() {
if (this.state === 'pending') {
return;
}
while (this.handlers.length) {
const { onFulfilled, onRejected, resolve, reject } = this.handlers.pop();
if (this.state === 'fulfilled') {
onFulfilled(this.result);
} else if (this.state === 'rejected') {
onRejected(this.result);
}
}
}
then(onFulfilled, onRejected, resolve, reject) {
return new Promise((resolve, reject) => {
this.handlers.push({
onFulfilled,
onRejected,
resolve,
reject,
});
this.run();
});
}
}
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
}, 300);
});
p.then(res => {
console.log('res-1', res);
return new Promise((resolve, reject) => {
resolve('这是第二个成功');
});
}).then(res => {
console.log('res-2', res);
});
总结
通过手动实现 MyPromise,我们深入理解了 Promise 的核心机制:
- 状态机:
pending→fulfilled/rejected的单向流转。 - 任务队列:
handlers存储异步回调,run方法触发执行。 - 链式调用:
then返回新Promise,实现.then().then()。 - 值穿透与错误处理: 确保链的健壮性。
- 微任务: 保证回调在正确的时间点异步执行。
虽然一个完整的 Promise 还有 catch、finally、Promise.resolve、Promise.reject、Promise.all 等方法,但掌握了 then 的实现,其他方法都迎刃而解。
下一步
- 实现
catch方法:catch(onRejected)等价于then(null, onRejected)。 - 实现静态方法
Promise.resolve()和Promise.reject()。 - 实现
Promise.all、Promise.race等组合方法。
手动实现 Promise 不仅是一次代码练习,更是一次对 JavaScript 异步编程思想的深刻探索。希望这篇博客能帮助你彻底掌握 Promise 的精髓!
上班心得
————————————————
上班真的好开心,需求bug来不停。
产品测试来回找,后端交互想上刑。
会议不停满楼跑,文档笔记要分清。
摸鱼睡觉一时爽,提测发布胆惊心。
大佬思绪跟得上,想法创意实践灵。
日常积累很重要,自我提高才算行。
暮然回首学生时,半载已过还未明。
处处少年何模样?如今胡须满颔停!
————————————————
更多推荐


所有评论(0)