这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战
由于 Promise 在异步编程中的重要性不言而喻,因此在很多面试中,现场实现 Promise 相关方法的题目经常会出现,比如 all、race 或者 any 等。因此一步步实现一个符合标准的 Promise,希望在遇到相关题目时能够游刃有余。
在开始前请先思考一下:
Promise/A+约定了哪些规范?- 在手动实现
Promise的过程中都遇到过哪些问题?
Promise/A+ 规范
只有对规范进行解读并且形成明确的认知,才能更好地实现 Promise。官方的地址为:promisesaplus.com/。
术语
先来看看 Promise/A+ 规范的基本术语,如下所示。
“promise” is an object or function with a then method whose behavior conforms to this specification. “thenable” is an object or function that defines a then method. “value” is any legal JavaScript value (including undefined, a thenable, or a promise). “exception” is a value that is thrown using the throw statement. “reason” is a value that indicates why a promise was rejected.
翻译过来,它所描述的就是以下五点。:
- “
promise”:是一个具有then方法的对象或者函数,它的行为符合该规范。 - “
thenable”:是一个定义了then方法的对象或者函数。 - “
value”:可以是任何一个合法的JavaScript的值(包括undefined、thenable或promise)。 - “
exception”:是一个异常,是在Promise里面可以用throw语句抛出来的值。 - “
reason”:是一个Promise里reject之后返回的拒绝原因。
状态描述
对 Promise 的内部状态的描述,如下所示。
A promise must be in one of three states: pending, fulfilled, or rejected. When pending, a promise: may transition to either the fulfilled or rejected state. When fulfilled, a promise: must not transition to any other state. must have a value, which must not change. When rejected, a promise: must not transition to any other state. must have a reason, which must not change. Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.
将上述描述总结起来,大致有以下几点。
- 一个
Promise有三种状态:pending、fulfilled和rejected - 当状态为
pending状态时,即可以转换为fulfilled或者rejected其中之一。 - 当状态为
fulfilled状态时,就不能转换为其他状态了,必须返回一个不能再改变的值。 - 当状态为
rejected状态时,同样也不能转换为其他状态,必须有一个原因的值也不能改变。
then 方法
一个 Promise 必须拥有一个 then 方法来访问它的值或者拒绝原因。
then 方法有两个参数:
promise.then(onFulfilled, onRejected)
onFulfilled 和 onRejected 都是可选参数。
- 如果
onFulfilled是函数,则当Promise执行结束之后必须被调用,最终返回值为value,其调用次数不可超过一次 onRejected除了最后返回的是reason外,其他方面和onFulfilled在规范上的表述基本一样
多次调用
then 方法其实可以被一个 Promise 调用多次,且必须返回一个 Promise 对象。then 的写法如下所示,其中 Promise1 执行了 then 的方法之后,返回的依旧是个 Promise2,然后我们拿着 Promise2 又可以执行 then 方法,而 Promise2 是一个新的 Promise 对象,又可以继续进行 then 方法调用。
promise2 = promise1.then(onFulfilled, onRejected);
一步步实现 Promise
按照 Promise/A+ 的规范,第一步就是构造函数。
构造函数
这一步的思路是:Promise 构造函数接受一个 executor 函数,executor 函数执行完同步或者异步操作后,调用它的两个参数 resolve 和 reject。请看下面的代码,大致的构造函数框架就是这样的。
function Promsie(executor) {
this.status = "pending"; // Promise当前的状态
this.data = undefined; // Promise的值
this.onResolvedCallBack = []; // Promise resolve时的回调函数集
this.onRejectedCallBack = []; // Promise reject时的回调函数集
executor(resolve, reject); // 执行executor并传入相应的参数
}
从上面的代码中可以看出,我们先定义了一个 Promise 的初始状态 pending,以及参数执行函数 executor,并且按照规范设计了一个 resolve 回调函数集合数组 onResolvedCallback 以及 一个 reject 回调函数集合数组onRejectedCallBack,那么构造函数的初始化就基本完成了。
接下来需要在构造函数中完善 resolve 和 reject 两个函数,完善之后的代码如下。
function Promsie(executor) {
this.status = "pending"; // Promise当前的状态
this.data = undefined; // Promise的值
this.onResolvedCallBack = []; // Promise resolve时的回调函数集
this.onRejectedCallBack = []; // Promise reject时的回调函数集
executor(resolve, reject); // 执行executor并传入相应的参数
const resolve = (value) => {
//
};
const reject = (value) => {
//
};
}
resolve 和 reject 内部应该怎么实现呢?我们根据规范知道这两个方法主要做的事情就是返回对应状态的值 value 或者 reason,并把 Promise 内部的 status 从 pending 变成对应的状态,并且这个状态在改变了之后是不可以逆转的。
function Promsie(executor) {
//上面省略
const resolve = (value) => {
if (this.status === "pending") {
this.status = "resolved";
this.data = value;
for (let i = 0; i < this.onResolvedCallBack.length; i++) {
this.onResolvedCallBack[i](value);
}
}
};
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.data = reason;
for (let i = 0;i<this.onRejectedCallBack.length;i++) {
this.onRejectedCallBack[i](reason)
}
}
};
}
上述代码所展示的,基本就是在判断状态为 pending 之后,把状态改为相应的值,并把对应的 value 和 reason 存在内部的 data 属性上面,之后执行相应的回调函数。逻辑比较简单,无非是由于 onResolveCallback 和 onRejectedCallback 这两个是数组,需要通过循环来执行。
实现 then 方法
根据标准,我们要考虑几个问题: then 方法是 Promise 执行完之后可以拿到 value 或者 reason 的方法,并且还要保持 then 执行之后,返回的依旧是一个 Promise 方法,还要支持多次调用(上面标准中提到过)。
Promsie.prototype.then = function (onResolved,onRejected){
const self = this;
let promise2;
// 根据标准,如果then的参数不是function,则忽略它
onResolved = typeof onResolved === "function" ? onResolved : function(v){};
onRejected = typeof onRejected === 'function' ? onRejected : function(v){};
if (self.status === 'resolved') {
return promise2 = new Promsie(function(resolve,reject){
});
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve,reject){
})
}
if (self.status === 'pending') {
return promise2 = new Promsie(function(resolve,reject){
})
}
}
在 then 方法内部先初始化了 Promise2 的对象,用来存放执行之后返回的 Promise,并且还需要判断 then 方法传参进来的两个参数必须为函数,这样才可以继续执行。
上面我只是搭建了 then 方法框架的整体思路,但是不同状态的返回细节处理也需要完善,通过仔细阅读标准,完善之后的 then 的代码如下。
Promsie.prototype.then = function (onResolved, onRejected) {
let promise2;
// 根据标准,如果then的参数不是function,则忽略它
onResolved = typeof onResolved === "function" ? onResolved : function (v) {};
onRejected = typeof onRejected === "function" ? onRejected : function (v) {};
if (this.status === "resolved") {
return (promise2 = new Promsie(function (resolve, reject) {
// 如果promise1的状态已经确定并且是resolved,我们调用onResolved,考虑到有可能throw,所以还需要将其包在try/catch块里
try {
let x = onResolved(this.data);
// 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
if (x instanceof Promsie) {
x.then(resolve, reject);
}
resolve(x); //否则,以它的返回值作为promise2的结果
} catch (e) {
reject(e); //如果出错,以捕获到的错误作为promise2的结果
}
}));
}
if (this.status === "rejected") {
return (promise2 = new Promise(function (resolve, reject) {
// 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数
try {
let x = onRejected(this.data);
if (x instanceof Promsie) {
x.then(resolve, reject);
}
} catch (e) {
reject(e);
}
}));
}
if (this.status === "pending") {
return (promise2 = new Promsie(function (resolve, reject) {
// 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,只能等到Promise的状态确定后,才能确定如何处理
this.onResolvedCallBack.push((resolve,reject)=>{
try {
let x = onResolved(this.data);
if (x instanceof Promise) {
x.then(resolve,reject);
}
}catch (e) {
reject(e);
}
})
this.onRejectedCallBack.push((resolve,reject)=>{
try {
let x = onRejected(this.data);
if (x instanceof Promsie) {
x.then(resolve,reject);
}
} catch (e) {
reject(e);
}
})
}));
}
};
根据上面的代码可以看出,我们基本实现了一个符合标准的 then 方法。但是标准里提到了,还要支持不同的 Promise 进行交互,关于不同的 Promise 交互其实Promise 标准说明中有提到。其中详细指定了如何通过 then 的实参返回的值来决定 Promise2 的状态。
关于为何需要不同的 Promise 实现交互,原因应该是 Promise 并不是 JS 一开始存在的标准,如果你使用的某一个库中封装了一个 Promise 的实现,想象一下如果它不能跟你自己使用的 Promise 实现交互的情况,其实还是会有问题的,因此我们还需要调整一下 then 方法中执行 Promise 的方法。
另外还有一个需要注意的是,在 Promise/A+ 规范中,onResolved 和 onRejected 这两项函数需要异步调用。
所以我们需要对代码做一点变动,即在处理 Promise 进行 resolve 或者 reject 的时候,加上 setTimeout(fn, 0)。
下面结合上面两点调整,给出完整版的代码
try {
module.exports = Promise;
} catch (e) {}
function Promise(executor) {
this.status = "pending";
this.onResolvedCallback = [];
this.onRejectedCallback = [];
const resolve = (value) => {
if (value instanceof Promise) {
return value.then(resolve, reject);
}
setTimeout(() => {
// 异步执行所有的回调函数
if (this.status === "pending") {
this.status = "resolved";
this.data = value;
for (let i = 0; i < this.onResolvedCallback.length; i++) {
this.onResolvedCallback[i](value);
}
}
});
};
const reject = (reason) => {
setTimeout(() => {
// 异步执行所有的回调函数
if (this.status === "pending") {
this.status = "rejected";
this.data = reason;
for (let i = 0; i < this.onRejectedCallback.length; i++) {
this.onRejectedCallback[i](reason);
}
}
});
};
try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}
function resolvePromise(promise2, x, resolve, reject) {
let then;
let thenCalledOrThrow = false;
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected for promise!"));
}
if (x instanceof Promise) {
if (x.status === "pending") {
x.then((v) => {
resolvePromise(promise2, v, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
if (x !== null && (typeof x === "object" || typeof x === "function")) {
try {
then = x.then;
if (typeof then === "function") {
then.call(
x,
(rs = (y) => {
if (thenCalledOrThrow) return;
thenCalledOrThrow = true;
return resolvePromise(promise2, y, resolve, reject);
}),
(rj = (r) => {
if (thenCalledOrThrow) return;
thenCalledOrThrow = true;
return reject(r);
})
);
} else {
resolve(x);
}
} catch (e) {
if (thenCalledOrThrow) return;
thenCalledOrThrow = true;
return reject(e);
}
} else {
resolve(x);
}
}
Promise.prototype.then = function (onResolved, onRejected) {
let promise2;
onResolved =
typeof onResolved === "function"
? onResolved
: function (v) {
return v;
};
onRejected =
typeof onRejected === "function"
? onRejected
: function (r) {
throw r;
};
if (this.status === "resolved") {
return (promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
// 异步执行onResolved
try {
let x = onResolved(this.data);
resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (this.status === "rejected") {
return (promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
// 异步执行onRejected
try {
let x = onRejected(this.data);
resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (this.status === "pending") {
// 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义
return (promise2 = new Promise((resolve, reject) => {
this.onResolvedCallback.push((value) => {
try {
let x = onResolved(value);
resolvePromise(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
this.onRejectedCallback.push((reason) => {
try {
let x = onRejected(reason);
resolvePromise(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
// 最后这个是测试用的,后面会说
Promise.deferred = Promise.defer = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
};
上面这段代码就是通过一步步优化调整出来的最终版,其中细节也是比较多的,介于篇幅问题,暂时能讲的点就先说这么多。如果你还有哪里不清楚的,最好还是动手实践去理解。
最终版的 Promise 的实现还是需要经过规范的测试(Promise /A+ 规范测试的工具地址为:github.com/promises-ap… deferred 方法(即 exports.deferred 方法),上面提供的代码中我已经将其加了进去。
最后,执行如下代码 npm 安装之后,即可执行测试。
npm i -g promises-aplus-tests
promises-aplus-tests Promise.js
总结
到这里,可以再思考一下 Promise /A+ 规范中的一些细节,以及实现过程中需要注意的问题,如果能够在面试中实现一个这样的 Promise,就基本可以满足岗位的需求了。
通过本次的学习,应该能理解 Promise 底层的实现逻辑了,虽然并不一定有场景适合落地业务,但是整个手动实现的过程对于 JS 编码能力的提升会很有帮助。