在现代 JavaScript 开发中,Promise 是处理异步操作的重要工具。通过学习手写一个符合 Promise/A+ 规范 的 Promise 实现,不仅可以帮助我们更好地理解异步编程的核心概念,还能增强我们解决复杂问题的能力。接下来,我们将从最基本的步骤开始,逐步实现一个简化版的 Promise,并详细解释每一个步骤。
Promise 的基本结构
首先,我们创建一个基础的 Promise 类。这个类的构造函数接受一个 executor 函数作为参数,并立即执行该函数。
class Promise {
constructor(executor) {
// 成功时的函数
let resolve = () => { };
// 失败时的函数
let reject = () => { };
// 立即执行传入的 executor 函数
executor(resolve, reject);
}
}
这种基本结构定义了一个 Promise 类,该类在实例化时立即执行 executor 函数,并传入两个参数:resolve 和 reject。
Step 1: 增加状态和结果处理
接下来,我们要处理 Promise 的三种状态,以及成功和失败时的结果。
初始化状态、值和原因
我们需要为这个 Promise 初始化状态(pending)、成功的值(value)和失败的原因(reason)。
class Promise {
constructor(executor) {
this.state = 'pending'; // 初始状态为 pending 等待态
this.value = undefined; // 成功时的值
this.reason = undefined; // 失败时的原因
// 成功时的函数
let resolve = (value) => {
if (this.state === 'pending') { // 只能从 pending 变为 fulfilled
this.state = 'fulfilled';
this.value = value;
}
};
// 失败时的函数
let reject = (reason) => {
if (this.state === 'pending') { // 只能从 pending 变为 rejected
this.state = 'rejected';
this.reason = reason;
}
};
// 立即执行传入的 executor 函数
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}
在这里,我们做了以下几件事:
- 初始化
state属性为pending,以表示初始状态。 - 初始化
value和reason属性分别用于存储成功的值和失败的原因。 - 定义
resolve和reject函数用于改变 Promise 的状态和结果。 - 立即执行
executor函数,并在执行过程中捕获任何错误,若有错误则调用reject函数。
Step 2: 增加状态变更的回调处理——解决异步场景
在现代应用中,异步操作(如 setTimeout)是非常常见的。因此,我们需要确保在 resolve 或 reject 被异步调用时,能够正确地处理这些情况。
当 Promise 处于 pending 状态时,应该存储所有的回调函数(onFulfilled 和 onRejected),等状态变为 fulfilled 或 rejected 时,再依次执行这些回调函数。
添加回调数组
我们需要为 Promise 添加两个数组,用来存储成功和失败的回调函数。这些回调函数会在 resolve 或 reject 被调用时执行:
class Promise {
constructor(executor) {
this.state = 'pending'; // 初始状态
this.value = undefined; // 成功的值
this.reason = undefined; // 失败的原因
this.onResolvedCallbacks = []; // 存储成功的回调
this.onRejectedCallbacks = []; // 存储失败的回调
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn()); // 执行所有成功的回调
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn()); // 执行所有失败的回调
}
};
try {
executor(resolve, reject); // 立即执行 executor
} catch (err) {
reject(err); // 如果执行器抛错,直接执行 reject
}
}
}
then 方法
为了在 Promise 状态变更时执行相应的回调函数,我们需要在 then 方法中处理这些回调。then 方法接收两个参数:onFulfilled 和 onRejected。如果 Promise 状态已经变为 fulfilled 或 rejected,则立即执行相应的回调。如果 Promise 仍处于 pending 状态,则将回调函数存储起来。
class Promise {
constructor(executor) {
// 构造函数代码略...
}
then(onFulfilled, onRejected) {
if (this.state === 'fulfilled') {
onFulfilled(this.value); // 成功态直接执行回调,并传入成功结果
} else if (this.state === 'rejected') {
onRejected(this.reason); // 失败态直接执行回调,并传入失败原因
} else if (this.state === 'pending') {
// pending 状态时,将 onFulfilled 和 onRejected 存入对应的回调数组
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value);
});
this.onRejectedCallbacks.push(() => {
onRejected(this.reason);
});
}
}
}
Step 3: 实现链式调用与确保异步执行
在现代应用中,Promise 的链式调用和异步回调是至关重要的。为了实现 .then().then() 这样的链式调用,我们需要对 then 方法进行改进,使其能够返回一个新的 Promise。同时,为了确保回调函数是异步执行的,我们使用 queueMicrotask。
为什么使用 queueMicrotask
根据 Promise 规范,onFulfilled 和 onRejected 回调必须在当前的执行栈完成之后异步执行。使用 queueMicrotask 可以确保回调函数在当前事件循环结束后立即执行,而不是等待下一个事件循环。这对于优化性能和确保正确的执行顺序非常重要,而不是使用 setTimeout。
使用 setTimeout 也可以实现异步执行,但它会在所有的宏任务完成后才会执行,因此会有较大的延时。而 queueMicrotask 更加高效,因为它会在当前事件循环结束后立即执行。
class Promise {
constructor(executor) {
// 构造函数代码略
}
then(onFulfilled, onRejected) {
const promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else if (this.state === 'rejected') {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
});
return promise2;
}
}
为什么 then 方法需要类型检查与默认处理
在 Promise 的 then 方法中,我们添加了关于 onFulfilled 和 onRejected 的类型检查和默认处理:
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
这些代码的作用是确保 then 方法中的回调函数是有效的函数,并为可能的空值赋予默认值。
-
onFulfilled:- 检查传入的
onFulfilled参数是否为函数。 - 如果
onFulfilled是有效的函数,则保持原样。 - 如果
onFulfilled不是函数(例如undefined),则替换为一个默认函数,该函数简单地返回传入的值:
value => value这种默认处理方式使得后续的
then方法能够继续操作,即使没有提供onFulfilled回调函数。promise.then(null, onRejected); // 等价于 promise.then(value => value, onRejected); - 检查传入的
-
onRejected:- 检查传入的
onRejected参数是否为函数。 - 如果
onRejected是有效的函数,则保持原样。 - 如果
onRejected不是函数(例如undefined),则替换为一个默认函数,该函数简单地抛出传入的原因:
reason => { throw reason }这种默认处理方式确保如果没有提供
onRejected回调函数,拒绝的原因能够向后传递,进而被后续的catch块处理。 - 检查传入的
为什么 then 方法必须返回一个新的 Promise
then 方法必须返回一个新的 Promise,以支持链式调用。链式调用是 Promise 的重要特性,允许我们将多个异步操作串联在一起,每个操作都依赖于前一个操作的结果。
当我们调用 then 方法时,它返回一个新的 Promise,这个新的 Promise 的状态取决于回调函数的执行结果。如果回调函数返回一个值,新的 Promise 将以这个值为结果。如果回调函数抛出错误,新的 Promise 将以这个错误为拒绝理由。
通过这种机制,我们可以连续调用多次 then 方法,每次都可以处理上一步的结果并返回一个新的 Promise,实现复杂的异步操作链。
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
}).then(value => {
console.log(value); // 1
return value * 2;
}).then(value => {
console.log(value); // 2
return value * 3;
}).then(value => {
console.log(value); // 6
}).catch(error => {
console.error(error);
});
通过返回一个新的 Promise,我们的代码能够以链式结构编写,每个 then 的返回值都可以传递给后续的 then,使代码更加清晰和易于维护。
Step 4: 解析 resolvePromise 函数
resolvePromise 函数在 Promise 实现中扮演着关键角色。它用于处理 then 方法返回的结果,并确保其符合 Promise/A+ 规范。
const resolvePromise = (promise2, x, resolve, reject) => {
if (x === promise2) {
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') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if (called) return;
called = true;
reject(err);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
};
下面详细解析每个判断的作用和原因。
确保避免循环引用
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
这个判断是为了避免循环引用。如果 promise2 和 x 是同一个对象,则会导致无限循环,从而引发栈溢出错误。例如,以下代码会导致无限递归:
let promise = new Promise((resolve, reject) => {
resolve();
}).then(() => {
return promise; // 无限循环
});
通过这个检查,我们可以防止这种情况,避免产生不可控的错误。
处理对象或函数
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
这个判断用于检测 x 是否是一个对象或函数。如果 x 是 null 或基本类型(如字符串、数字等),则直接调用 resolve 进行处理。如果 x 是对象或函数,则可能是一个 "thenable" 对象,我们需要进一步处理。例如,以下代码中传入了一个 thenable 对象:
let promise = new Promise((resolve, reject) => {
resolve({
then: function (onFulfilled) {
onFulfilled('Hello');
}
});
});
检查并处理 then 方法
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if (called) return;
called = true;
reject(err);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
在这段代码中,我们尝试读取 x 的 then 方法,以确定 x 是否是一个 "thenable" 对象。如果 then 是一个函数,我们将 x 视为一个 Promise,并调用 then 方法。这样可以递归处理返回的新 Promise。如果 then 不是函数,则将 x 视为普通值,直接调用 resolve。
防止多次调用
let called = false;
...
if (called) return;
called = true;
变量 called 用于防止回调函数被多次调用。这在处理 "thenable" 对象时尤其重要,因为根据 Promise/A+ 规范,一旦 resolve 或 reject 被调用,状态就应被锁定,而 then 方法中的回调函数可能会被多次调用。
举个例子:
let obj = {
then(resolve, reject) {
resolve('First call');
resolve('Second call');
}
};
new Promise((resolve, reject) => {
resolve(obj);
}).then(value => {
console.log(value); // 输出: 'First call'
});
在这个例子中,obj 的 then 方法中多次调用 resolve。由于 called 变量的存在,仅第一次调用有效,后续的调用将被忽略。
完整示例
通过 resolvePromise 函数,我们确保 then 方法的返回值能够正确处理和传递,并保证新返回的 Promise 符合 Promise/A+ 规范,从而实现链式调用和正确的异步行为。以下是完整的代码实现,包括对回调函数的异步处理(使用 queueMicrotask)和 then 方法返回新的 Promise:
const resolvePromise = (promise2, x, resolve, reject) => {
if (x === promise2) {
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') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if (called) return;
called = true;
reject(err);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
};
class Promise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
};
const reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
const promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else if (this.state === 'rejected') {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
} else if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
});
return promise2;
}
catch(fn) {
return this.then(null, fn);
}
}
如何验证
在实际应用中,往往需要验证我们实现的 Promise 是否符合规范和预期。可以使用 promises-aplus-tests 插件来进行验证。
-
安装
promises-aplus-tests插件:npm install -g promises-aplus-tests -
在我们的代码结尾添加以下代码:
Promise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; } module.exports = Promise; -
在我们的命令行运行测试:
promises-aplus-tests path/to/your/promise-file.js