手撕Promise
手写一个Promise已经是一个常见的手写功能了,虽然实际工作上可能并不会用到。但是在面试时还是会经常被提起的。
看完收获
- 实现一个手写promise
- 对promise执行流程有着更深刻的理解
- 从底层理解proimse,应对各种面试题
Promise分析
作用
通过链式调用的方式,解决回调地狱的问题。
特点
-
是ES6中新增的引用类型,通过new关键词来创建实例。
-
存在3个状态:
- pending
- fulfilled
- rejected
-
且状态变成fulfilled或者rejected后不可再次变更,具有不可逆性。
-
new Promise创建实例的时候必须要传入一个函数,且这个函数会有2个参数
resolve
和reject
...
总体实现
首先我们可以按照Promise的特点来实现一个简单的总体结构。
- 他是一个构造函数,每个创建的promise都有各自状态和值,且状态初始值为pending,值为undefined。
- 创建实例的时候需要传入一个函数,而这个函数可以接受2个函数作为参数,这2个函数都有一个参数,且都可以更改实例的状态和值。
- 根据Promise的结构,我们可以发现then()、catch()方法在它的原型上。
function MyPromise(fn) {
this.PromiseState = 'pending'
this.PromiseResult = undefined;
function resolve(data) {
}
function reject(error) {
}
}
MyPromise.prototype.then = function(thenCallback) {
}
MyPromise.prototype.catch = function(catchCallback) {
}
上述代码很简单,就定义了一个Promise的构造函数,有2个属性(PromiseState、PromiseResult)和2个方法(resolve、reject)。
原型上增加了then方法和catch方法,且这2个方法都接收一个函数。
而每次通过new来创建Promise的函数时,传入的函数会执行,因此我们直接调用fn函数,并传入resolve和reject这2个函数。
resolve和reject初步实现
resolve的调用会更改promise的状态和值,且状态是不可逆的。也就是说,只能从pending变成fulfilled或者rejected。而resolve函数的参数值会变成promise实例的值,reject同理。 所以我们把resolve和reject简单的写完整.
function MyPromise(fn) {
this.PromiseState = "pending";
this.PromiseResult = undefined;
// 保存实例对象的this的值
const self = this;
function resolve(data) { // 如果不使用self,这里内部的this会指向window
// 如果当前的promise实例不是pending的状态就退出了,否则就更改当前的promise实例的状态和值
if (self.PromiseState !== "pending") {
return;
}
// 1.修改对象的状态([[promiseState]])
// 2.设置对象结果值([[promiseResult]])
self.PromiseState = "fulfilled";
self.PromiseResult = data;
}
function reject(error) {
if (self.PromiseState !== "pending") {
return;
}
self.PromiseState = "rejected";
self.PromiseResult = error;
}
fn(resolve, reject);
}
上面代码中,resolve和reject就是用来更改当前实例的状态的,如果当前状态已经改变了,即不为pending,那么就不再进行状态变更和值变更了。
接下来就是验证一下:
const p = new MyPromise((resolve, reject) => {
resolve(1);
reject(2);
});
console.log(p);
发现我们执行了resolve后再执行reject并不会再次更改状态。
但是还有一点,如果我们不执行resolve或者reject,而是直接执行throw呢?
先看看原生的Promise效果。
const p1 = new Promise((resolve, reject) => {
throw 111;
});
console.log(p1);
发现状态会变成rejected。
那我们是不是可以在执行fn(resolve,reject)的时候进行异常处理,将状态变成rejected,值变成该异常信息。
// 如果throw异常,就需要捕获到异常然后更改状态
try {
// 同步调用执行器函数
fn(resolve, reject);
} catch (e) {
// 修改promise对象的状态为失败
reject(e);
}
then方法的实现
then方法中我们知道它有2个参数,且这2个参数都是函数,第一个参数会在promise执行resolve时调用,第二个参数会在promise执行reject时调用。而.catch只不过是调用.then第二个参数的语法糖而已。
const p = new Promise((resolve, reject) => {
resolve(1);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
console.log(p, 111);
console.log(p1, 222);
但是我们查看原生的Promise后,可以发现,.then的执行会返回一个新的Promise实例。而决定调用.then的第几个参数,则是根据调用then的那个promise实例的状态决定。
修改代码如下:
MyPromise.prototype.then = function (thenCallback, catchCallback) {
return new Promise((resolve, reject) => {
// 调用回调函数,要根据当前的promise实例来调用
if (this.PromiseState === "fulfilled") {
const result = thenCallback(this.PromiseResult);
resolve(result);
}
if (this.PromiseState === "rejected") {
const result = catchCallback(this.PromiseResult);
resolve(result);
}
});
};
我们的自定义函数上的原型的then返回了一个新的promise实例,而新的promise实例调用then的第几个参数则根据调用then的promise实例的状态决定(此处的this指向调用then的那个promise实例)。
我们需要拿到回调函数的执行结果,然后再放入新的promise实例的resolve中更改新实例的状态和值。
查看一下我们自己的效果
const p = new MyPromise((resolve, reject) => {
reject(1);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
console.log(p, 111);
console.log(p1, 222);
这时候可能有人就会提出疑问,如果在上述的p中的回调里延时执行resolve或者reject呢?
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
console.log(p, 111);
console.log(p1, 222);
我们可以发现p1的状态变成了pending。
这是因为当执行到setTimeout的时候,发现是一个异步函数,然后会将这个函数挂起,继续执行下面的.then(),然后当时的p的状态是pendding,因为不会执行任何代码。
那我们当然是要加一个pendding的判断啦!
可是该怎么实现判断的内部代码呢?
首先我们先来思考一下,promise是一个对象,他是按地址引用的。我们希望在定时器时间到的时候再去更改这个新的promise的状态和值。因此我们需要访问到上一个promise的状态和值,并且在resolve或者reject执行的时候访问到,而不是定时器挂起的时候。
function MyPromise(fn) {
this.PromiseState = "pending";
this.PromiseResult = undefined;
this.callback = {};
// 保存实例对象的this的值
const self = this;
function resolve(data) {
if (self.PromiseState !== "pending") {
return;
}
// 1.修改对象的状态([[promiseState]])
// 2.设置对象结果值([[promiseResult]])
self.PromiseState = "fulfilled";
self.PromiseResult = data;
if (self.callback.onResolved) {
self.callback.onResolved(data);
}
}
function reject(error) {
if (self.PromiseState !== "pending") {
return;
}
self.PromiseState = "rejected";
self.PromiseResult = error;
if (self.callback.onReject) {
self.callback.onReject(error);
}
}
// 如果throw异常,就需要捕获到异常然后更改状态
try {
// 同步调用执行器函数
fn(resolve, reject);
} catch (e) {
// 修改promise对象的状态为失败
reject(e);
}
}
MyPromise.prototype.then = function (thenCallback, catchCallback) {
return new Promise((resolve, reject) => {
// 调用回调函数,要根据当前的promise实例来调用
if (this.PromiseState === "fulfilled") {
const result = thenCallback(this.PromiseResult);
resolve(result);
}
if (this.PromiseState === "rejected") {
const result = catchCallback(this.PromiseResult);
resolve(result);
}
// 判断pending状态
if (this.PromiseState === "pending") {
// 保存回调函数
this.callback = {
onResolved: function (data) {
let result = thenCallback(data);
resolve(result);
},
onRejected: function (error) {
let result = catchCallback(error);
resolve(result);
},
};
}
});
};
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
console.log(p, 111);
console.log(p1, 222);
因为new Promise的实例中遇到了异步的setTimeout会将内部的程序挂起,继续执行.then。
而这时promise实例的状态还是pending,因此我们需要对pending的状态进行处理,我们主要目的是,在setTimeout的回调函数执行的时候,能够执行.then的函数参数。
这时候就需要进行一个巧妙的设计,我们需要在MyPromise构造函数中增加一个callback的属性,他是一个对象。
同时我们需要在pending的时候在.then里面将then的两个参数传给callback对象,同时传入resolve的值或者是reject的值。
在pending的时候我们将函数传给了实例的callback,当被挂起的setTimeout回调函数执行的时候,这时候callback已经有值了,所以会根据resolve或者是reject去执行对应的函数,并且将promise的实例的值传给对应的函数。
那如我我对p多次调用.then或者.catch呢?
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
const p2 = p.then((res) => {
console.log(res, "成功2");
});
console.log(p1, p2);
我们发现我们的值执行了一次,那我们看一下原生的,可以发现会执行2次。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
const p2 = p.then((res) => {
console.log(res, "成功2");
});
console.log(p1, p2);
原因很简单,因为我们的callback属性是一个对象,每次执行.then都会覆盖当前实例原先的callback。
怎么结局呢?很简单,我们只要将每次的pending时设置callback改成向数组加入对象就好。
调用时我们通过遍历去调用就好了。
function MyPromise(fn) {
this.PromiseState = "pending";
this.PromiseResult = undefined;
this.callbacks = [];
// 保存实例对象的this的值
const self = this;
function resolve(data) {
if (self.PromiseState !== "pending") {
return;
}
// 1.修改对象的状态([[promiseState]])
// 2.设置对象结果值([[promiseResult]])
self.PromiseState = "fulfilled";
self.PromiseResult = data;
//调用成功的回调函数
self.callbacks.forEach((item) => {
item.onResolved(data);
});
}
function reject(error) {
if (self.PromiseState !== "pending") {
return;
}
self.PromiseState = "rejected";
self.PromiseResult = error;
//执行失败的回调
self.callbacks.forEach((item) => {
item.onRejected(error);
});
}
// 如果throw异常,就需要捕获到异常然后更改状态
try {
// 同步调用执行器函数
fn(resolve, reject);
} catch (e) {
// 修改promise对象的状态为失败
reject(e);
}
}
MyPromise.prototype.then = function (thenCallback, catchCallback) {
return new Promise((resolve, reject) => {
// 调用回调函数,要根据当前的promise实例来调用
if (this.PromiseState === "fulfilled") {
const result = thenCallback(this.PromiseResult);
resolve(result);
}
if (this.PromiseState === "rejected") {
const result = catchCallback(this.PromiseResult);
resolve(result);
}
// 判断pending状态
if (this.PromiseState === "pending") {
// 保存回调函数
this.callbacks.push({
onResolved: thenCallback,
onRejected: catchCallback,
});
}
});
};
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
},
(err) => {
console.log(err, "失败");
}
);
const p2 = p.then((res) => {
console.log(res, "成功2");
});
console.log(p1, p2);
然后我们就发现我们可以正常地调用了。
那如果.then返回的值是一个promise呢?
这时候我们就需要考虑一下,如果.then返回时一个promise实例或者是一个throw错误呢?
const p = new Promise((resolve, reject) => {
resolve(1);
});
const p1 = p.then(
(res) => {
console.log(res, "成功");
return new Promise((r, j) => {
r(res);
});
},
(err) => {
console.log(err, "失败");
}
);
console.log(p1, "p1");
我们先看一下原生的效果,发现他时拿到了返回的promise的值。
那是不是就是在调用then的回调函数的时候判断一下返回的是不是一个promise,如果是promise我们在进行特殊处理。
// 将.then的判定方式改成如下
if (this.PromiseState === "fulfilled") {
try {
const result = thenCallback(this.PromiseResult);
if (result instanceof MyPromise) {
result.then(
(r) => {
resolve(r);
},
(j) => {
reject(j);
}
);
} else {
resolve(result);
}
} catch (e) {
reject(e);
}
}
if (this.PromiseState === "rejected") {
try {
const result = catchCallback(this.PromiseResult);
if (result instanceof MyPromise) {
result.then(
(r) => {
resolve(r);
},
(j) => {
reject(j);
}
);
} else {
resolve(result);
}
} catch (e) {
reject(e);
}
}
如果.then的回掉函数执行后返回的是一个promise实例,但是我们.then默认就返回了一个实例,我们就直接取这个实例的resolve值或者是reject值,将返回值给then默认返回的promise实例。
异步方式同理,但是要注意this的指向问题。
// 判断pending状态
if (this.PromiseState === "pending") {
// 保存回调函数
this.callbacks.push({
onResolved: function () {
try {
// 这里的self指向的是调用.then的promise实例
let result = thenCallback(self.PromiseResult);
//判断
if (result instanceof Promise) {
result.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
}
);
} else {
resolve(result);
}
} catch (e) {
reject(e);
}
},
onRejected: function () {
try {
// 这里的self指向的是调用.then的promise实例
let result = catchCallback(self.PromiseResult);
//判断
if (result instanceof Promise) {
result.then(
(v) => {
resolve(v);
},
(r) => {
reject(r);
}
);
} else {
resolve(result);
}
} catch (e) {
reject(e);
}
},
});
}
});
我们发现,代码已经重复了4次,我们就可以进行一个封装。
MyPromise.prototype.then = function (thenCallback, catchCallback) {
const self = this;
return new Promise((resolve, reject) => {
function run(type) {
try {
//获取回调函数的执行结果
let result = type(self.PromiseResult);
//判断
if (result instanceof Promise) {
//如果是 Promise 类型的对象
result.then(
(v) => {
resolve(v);
},
(j) => {
reject(j);
}
);
} else {
//结果的对象状态为『成功』
resolve(result);
}
} catch (e) {
reject(e);
}
}
// 调用回调函数,要根据当前的promise实例来调用
if (this.PromiseState === "fulfilled") {
run(thenCallback);
}
if (this.PromiseState === "rejected") {
run(catchCallback);
}
// 判断pending状态
if (this.PromiseState === "pending") {
// 保存回调函数
this.callbacks.push({
onResolved: function () {
run(thenCallback);
},
onRejected: function () {
run(catchCallback);
},
});
}
});
};
嗯,封装完后的一个完整的.then。我们先看一下完整的效果。
const p1 = p.then(
(res) => {
console.log(res, "成功");
return new Promise((r, j) => {
r(res);
});
},
(err) => {
console.log(err, "失败");
}
);
console.log(p1, "p1");
和原生的基本上是一模一样了。
接下来就是对catch的实现了
但是我们知道,catch其实就是.then执行第二个参数的一个语法糖。 因此,我们就可以将接收到的回调函数直接给.then的第二个参数。
MyPromise.prototype.catch = function(onRejected){
return this.then(undefined, onRejected);
}
这时候我们就要注意了,我们给then的第一个参数赋值了一个undefined。但是我们并没有对undefined进行处理。而且我们原生的Promise是可以传入其他数据的,不一定是一个回调函数。
因此,我们需要在MyPromise的原型里的.then方法进行一个判断:
if (typeof catchCallback !== "function") {
catchCallback = (reason) => {
throw reason;
};
}
if (typeof thenCallback !== "function") {
thenCallback = (value) => value;
}
如果传入的不是一个函数,我们就将它变成一个函数,并且获取的是上一个.then返回的promise实例的值。
总体上我们大致完成了,但是Promise还存在构造函数的方法Promise.resolve(value)
和Promise.reject(error)
这个就很简单了
//添加 resolve 方法
MyPromise.resolve = function(value){
//返回promise对象
return new MyPromise((r, j) => {
if(value instanceof MyPromise){
value.then(v=>{
r(v);
}, r=>{
j(r);
})
}else{
//状态设置为成功
resolve(value);
}
});
}
判断resolve传入的值是否是一个MyPromise实例,如果是,则根据这个Promise的状态来返回,如果不是则直接调用resolve()
reject同理
//添加 reject 方法
MyPromise.reject = function(error){
return new MyPromise((resolve, reject)=>{
reject(error);
});
}
Promise.all的实现
Promise.all的实现基于我们原先封装的MyPromise。
我们先来分析一下原先的Promise.all的方法,它接受一个promise数组,返回一个新的promise。如果数组中有一个状态是rejected,那将直接返回一个rejected的promise实例。如果都成功了,则返回成功的promise。
MyPromise.all = function (promiseLists) {
return new Promise((resolve, reject) => {
let promiseResults = [];
let count = 0;
for (let i = 0; i < promiseLists.length; i++) {
promiseLists.then(
(v) => {
count += 1;
promiseResults[i] = v;
if (count === promiseLists.length) {
resolve(promiseResults);
}
},
(err) => {
reject(err);
}
);
}
});
};
实现上我们定义了一个promiseLists的形参用来接收promise的数组。
因为返回的肯定是一个promise,所以我们直接返回了一个promise的实例。
我们定义了一个count用来计数成功执行的个数,promiseResults则用来接收成功的结果(要按照顺序接收)。
最后就是遍历promiseLists的数组了,如果是resolve则count+1,并且将值往promiseResults里面塞。
如果count的值和接受的数组长度一样了,那就是全部的promise返回了fulfilled。
如果有一个错误,则直接退出,并且将错误的promise的值传给all返回的promise。