最近在看手写API的面试题,发现了一个手写Promise,之前学长讲课的时候讲过,不过当时我还是刚刚接触前端,还没有学那么多,所以学长讲的,也没有听懂,时隔一年多,我已经大三了,也该研究一下这么写了。 网上挺多手写promise的文章,但是我试了几个没有多少完全通过官方promises-aplus-tests测试库的全部案例的,而且大部分代码完全一样,没有讲解为什么这样做,这就导致在看的时候稀里糊涂的。这就来总结一下
实现目标
要想手写promise,我们先要知道原生promise的一些方法,由此来照猫画虎,实现我们自己的一个promise方法,另外我们实现promise是按照Promise A+的规范来写的,具体可以看Promise/A+文档
- 函数的基本使用
- resolve、resject方法
- then方法
- catch方法
- finally方法
- MyPromise.resolve和MyPromise.reject静态方法
- MyPromise.all和MyPromise.race静态方法
- MyPromise.allSettled和MyPromise.all静态方法
- 使用promises-aplus-tests测试并通过官方案例
MyPromise实现
定义状态
我们都知道原生的promise有三种状态pending(等待)、fulfilled(已实现)、rejected(已失败),并且一旦改变状态之后不可再次改变
javascript
const status = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
完成类基本构造
因为我们平常在用promise的时候经常会用到new Promise,这么我们就可以看出来promise实际上是一个构造函数或者类,为了方便,我们这里使用类来写,我们在new的一般会传入一个回调函数,该函数接收两个回调函数作为参数,一个是resolve,一个是reject,接下来我们就来实现这些
javascript
const status = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class MyPromise {
constructor(executor) {
// 初始化类的基本操作
this.init()
// 定义resolve和reject函数
const resolve = (successValue) => {
// 当且仅当状态为padding时才会触发
if (this.status !== status.PENDING) {
return
}
this.status = status.FULFILLED
this.value = successValue
this.successFns.forEach(callBack => callBack())
}
const reject = (failValue) => {
// 当且仅当状态为padding时才会触发
if (this.status !== status.PENDING) {
return
}
this.status = status.REJECTED
this.reason = failValue
this.failFns.forEach(callBack => callBack())
}
// 执行传入的函数,当该函数抛出移除或者出错时,直接失败
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
init() {
// 初始化状态
this.status = status.PENDING
// 用于存放成功的值
this.value = null
// 用于存放成功的回到函数
this.successFns = []
// 用于存放出错的值
this.reason = null
// 用于存放失败的回调函数
this.failFns = []
}
}
then方法
then方法,也是接收两个函数,一个是成功的回调,另一个是失败的回调,只不过我们平常为了方便,不会传入第二个函数
javascript
then(successFn, failFn) {
// 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
if (this.status === status.PENDING) {
// 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
this.successFns.push(() => {
setTimeout(() => {
// 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
successFn(this.successRes)
})
})
this.failFns.push(() => {
setTimeout(() => {
failFn(this.failRes)
})
})
}
// 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
if (this.status === status.FULFILLED) {
setTimeout(() => {
successFn(this.successRes)
})
}
// 如果当前状态已经是失败的状态,就直接执行
if (this.status === status.REJECTED) {
setTimeout(() => {
failFn(this.failRes)
})
}
}
另外需要注意一下,我们上边实现的then方法,仍有不足,就是我们一般调用then方法时,我们可以链式调用,这就需要我们返回一个promise方法
修改then方法
javascript
then(successFn, failFn) {
// 如果不传处理函数,则使用默认处理函数
successFn = typeof successFn === 'function' ? successFn : value => value;
failFn = typeof failFn === 'function' ? failFn : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
// 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
if (this.status === status.PENDING) {
// 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
this.successFns.push(() => {
setTimeout(() => {
try {
// 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
successFn(this.value)
} catch (error) {
reject(error)
}
})
})
this.failFns.push(() => {
setTimeout(() => {
try {
failFn(this.reason)
} catch (error) {
reject(error)
}
})
})
}
// 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
if (this.status === status.FULFILLED) {
setTimeout(() => {
try {
successFn(this.value)
} catch (error) {
reject(error)
}
})
}
// 如果当前状态已经是失败的状态,就直接执行
if (this.status === status.REJECTED) {
setTimeout(() => {
try {
failFn(this.reason)
} catch (error) {
reject(error)
}
})
}
})
return promise2
}
不过我们修改后,仍然不对,因为我们then传入的函数中仍然可以返回一个promise方法,所以我们需要拿到传入函数返回的promise的结果,然后作为promise2的返回结果,这时候可能有人会疑惑为什么不能返回自身而是新建一个promise,这个是因为自身的状态已经改变过了,就不能再改变了。另外还有一种疑惑就是我们为什么不判断传入函数的执行结果,然后判断它是否返回一个promise函数,然后再返回,这是因为传入函数的执行是异步的,而then的链式调用是同步的,所以我们需要立马返回一个promise,并拿到返回的结果值(包括promise的返回结果)然后作为我们创建的promise2的返回值,以此达到链式调用的功能
链式调用
javascript
then(successFn, failFn) {
// 如果不传处理函数,则使用默认处理函数
successFn = typeof successFn === 'function' ? successFn : value => value;
failFn = typeof failFn === 'function' ? failFn : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
// 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
if (this.status === status.PENDING) {
// 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
this.successFns.push(() => {
setTimeout(() => {
try {
// 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
const x = successFn(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
this.failFns.push(() => {
setTimeout(() => {
try {
const x = failFn(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
}
// 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
if (this.status === status.FULFILLED) {
setTimeout(() => {
try {
const x = successFn(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
// 如果当前状态已经是失败的状态,就直接执行
if (this.status === status.REJECTED) {
setTimeout(() => {
try {
const x = failFn(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
})
return promise2
}
这里我们定义了一个resolvePromise用来递归的拿到返回的结果的值
javascript
function resolvePromise(promise2, x, resolve, reject) {
// 避免出现自己等待自己的情况
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 多次调用resolve或reject以第一次为主,忽略后边的
let called = false
// 判断传入的x是否是一个包含then方法的对象,如果有,就认为resolve返回的值是一个promise
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then
if (typeof then === 'function') {
// 认为是一个promise
then.call(
x,
y => {
if (called) {
return
}
called = true
// 递归执行,避免resolve是一个promise值
resolvePromise(promise2, y, resolve, reject)
},
reason => {
if (called) {
return
}
called = true
reject(reason)
})
} else {
resolve(x)
}
} catch (error) {
if (called) {
return
}
called = true
reject(error)
}
} else {
// 其他值,可以直接返回
resolve(x)
}
}
由此我们完成了手写promise中最复杂的一个功能,另外需要注意一点的是,可能有人不明白既然我们调用了resolve那么它的状态就会改变,为什么还需要called变量来过滤,其实很多博客说的都是避免重复调用,但是我看这个的时候比较迷,就是成功或者失败的函数的执行时机只能是statues为pedding的时候,怎么可能会重复调用呢,其实这个说法不太准确,准确的应该按Promise/A+规范中2.3.3.3中说的
If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored. 如果同时调用 resolvePromise 和 rejectPromise ,或者对同一参数进行多次调用,则第一个调用优先,并且忽略任何后续调用。
即:我们重复调用resolve或者reject应该以第一次的为准,而忽略后续的,我们可以看这种情况理解一下
javascript
const p = new MyPromise(resolve => {
resolve()
})
const thenable1 = {
then(reslove) {
setTimeout(() => {
reslove(2)
}, 0)
},
}
const thenable2 = {
then(resolve) {
resolve(thenable1)
resolve(1)
},
}
p.then(() => {
return thenable2
})
.then(res => {
console.log(res);
})
按上边规范所说的情况,我们应该最终得到的结果是2,但是如果没有called的情况,我们会得到1 这个很好理解,我们应该以第一个resolve为主,但是第一个resolve的值是一个带resolve函数的对象,并且用一个宏任务setTimeout来包裹,所以其执行时机会比resolve(2)要晚一步,那么就会错误的拿到1,这时候我们需要忽略后续的调用而采用第一次调用
完成其他方法
javascript
static resolve(value) {
// 传入的是一个promise
if (value instanceof MyPromise) {
return value
}
return new MyPromise((resolve, reject) => {
resolve(value)
})
}
static reject(err) {
return new MyPromise((resolve, reject) => {
reject(err)
})
}
catch(failFn) {
return this.then(null, failFn)
}
finally(callback) {
// 调用then方法,传入两个相同的处理函数
return this.then(
value => {
// 创建一个新的Promise实例,确保异步执行callback
return MyPromise.resolve(callback()).then(() => value);
},
reason => {
// 创建一个新的Promise实例,确保异步执行callback
return MyPromise.resolve(callback()).then(() => { throw reason; });
}
);
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const res = []
let conunt = 0
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(value => {
res[index] = value
conunt++
if (conunt === promises.length) {
resolve(res)
}
}, err => {
reject(err)
})
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(value => {
resolve(value)
}, err => {
reject(err)
})
})
})
}
static allSettled(promises) {
const result = [];
let settledCount = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
result[index] = { status: 'fulfilled', value };
settledCount++;
if (settledCount === promises.length) {
resolve(result);
}
},
reason => {
result[index] = { status: 'rejected', reason };
settledCount++;
if (settledCount === promises.length) {
resolve(result);
}
}
);
});
}
static any(promises) {
return new MyPromise((resolve, reject) => {
const errors = [];
let rejectedCount = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
resolve(value);
},
reason => {
errors[index] = reason;
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
});
});
}
完整代码
由此我们就完成了手写promise,并且完成了其中的一些方法
javascript
const status = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
function resolvePromise(promise2, x, resolve, reject) {
// 避免出现自己等待自己的情况
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 多次调用resolve或reject以第一次为主,忽略后边的
let called = false
// 判断传入的x是否是一个包含then方法的对象,如果有,就认为resolve返回的值是一个promise
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then
if (typeof then === 'function') {
// 认为是一个promise
then.call(
x,
y => {
if (called) {
return
}
called = true
// 递归执行,避免resolve是一个promise值
resolvePromise(promise2, y, resolve, reject)
},
reason => {
if (called) {
return
}
called = true
reject(reason)
})
} else {
resolve(x)
}
} catch (error) {
if (called) {
return
}
called = true
reject(error)
}
} else {
// 其他值,可以直接返回
resolve(x)
}
}
class MyPromise {
constructor(executor) {
// 初始化类的基本操作
this.init()
// 定义resolve和reject函数
const resolve = (successValue) => {
// 当且仅当状态为padding时才会触发
if (this.status !== status.PENDING) {
return
}
this.status = status.FULFILLED
this.value = successValue
this.successFns.forEach(callBack => callBack())
}
const reject = (failValue) => {
// 当且仅当状态为padding时才会触发
if (this.status !== status.PENDING) {
return
}
this.status = status.REJECTED
this.reason = failValue
this.failFns.forEach(callBack => callBack())
}
// 执行传入的函数,当该函数抛出移除或者出错时,直接失败
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
init() {
// 初始化状态
this.status = status.PENDING
// 用于存放成功的值
this.value = null
// 用于存放成功的回到函数
this.successFns = []
// 用于存放出错的值
this.reason = null
// 用于存放失败的回调函数
this.failFns = []
}
then(successFn, failFn) {
// 如果不传处理函数,则使用默认处理函数
successFn = typeof successFn === 'function' ? successFn : value => value;
failFn = typeof failFn === 'function' ? failFn : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
// 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
if (this.status === status.PENDING) {
// 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
this.successFns.push(() => {
setTimeout(() => {
try {
// 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
const x = successFn(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
this.failFns.push(() => {
setTimeout(() => {
try {
const x = failFn(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
}
// 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
if (this.status === status.FULFILLED) {
setTimeout(() => {
try {
const x = successFn(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
// 如果当前状态已经是失败的状态,就直接执行
if (this.status === status.REJECTED) {
setTimeout(() => {
try {
const x = failFn(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
})
return promise2
}
static resolve(value) {
// 传入的是一个promise
if (value instanceof MyPromise) {
return value
}
return new MyPromise((resolve, reject) => {
resolve(value)
})
}
static reject(err) {
return new MyPromise((resolve, reject) => {
reject(err)
})
}
catch(failFn) {
return this.then(null, failFn)
}
finally(callback) {
// 调用then方法,传入两个相同的处理函数
return this.then(
value => {
// 创建一个新的Promise实例,确保异步执行callback
return MyPromise.resolve(callback()).then(() => value);
},
reason => {
// 创建一个新的Promise实例,确保异步执行callback
return MyPromise.resolve(callback()).then(() => { throw reason; });
}
);
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const res = []
let conunt = 0
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(value => {
res[index] = value
conunt++
if (conunt === promises.length) {
resolve(res)
}
}, err => {
reject(err)
})
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(value => {
resolve(value)
}, err => {
reject(err)
})
})
})
}
static allSettled(promises) {
const result = [];
let settledCount = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
result[index] = { status: 'fulfilled', value };
settledCount++;
if (settledCount === promises.length) {
resolve(result);
}
},
reason => {
result[index] = { status: 'rejected', reason };
settledCount++;
if (settledCount === promises.length) {
resolve(result);
}
}
);
});
}
static any(promises) {
return new MyPromise((resolve, reject) => {
const errors = [];
let rejectedCount = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
resolve(value);
},
reason => {
errors[index] = reason;
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
});
});
}
}
module.exports = {
MyPromise
}
测试
写完了代码我们还需要测试我们写的代码是否符合promise/A+规范,我们可以使用promises-aplus-tests来测试
- 初始化项目
bash
npm init --y
- 安装依赖
bash
yarn add promises-aplus-tests -D
- 新建adapter.js
javascript
const { MyPromise } = require('./MyPromise')
// 暴露适配器对象
module.exports = {
resolved: MyPromise.resolve,
rejected: MyPromise.reject,
deferred() {
const result = {};
result.promise = new MyPromise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
};
- 新建test.js
javascript
const promisesAplusTests = require('promises-aplus-tests');
const adapter = require('./adapter');
promisesAplusTests(adapter, function (err) {
if (err) {
console.error('Promises/A+ 测试失败:');
console.error(err);
} else {
console.log('Promises/A+ 测试通过');
}
});
- 执行测试
javascript
node test.js
这样我们就查看我们的代码是否符合promise/A+的规范了
总结
我们完成了测试,就表示我们写的一个符合Promise/A+规范的promise,另外我们也补充了一些promise的一些基本方法,虽然有些人说手写API没啥用,不过我不这样认为,因为我们手写API的过程中,不仅能更多的了解到原生API的一些方法的实现原理,这样可以帮助我们遇到问题时更快速的定位,另外也给我们一个思路,手写promise的过程中我感觉它有点像一个发布订阅模式,同时还要考虑一些异步的问题,我这里是用setTimeout来简单模拟的,还可以用一个微任务队列去模拟,这个更多的是体会思想吧。真的不得不服知道标准的哪些人。