前言
大家好,我是麦当当,Promise想必在JavaScript领域中可谓是神一般存在的人物。它存在于axios中,也存在于浏览器和Node.js服务端。要是你不懂它的话,那么你就low了;特别是在面试中常来Promise的面试题,甚至面试官放出大招:“请你手写一个promise”。此时心中来个广东素质三连给他也不是不可...。如何实现一个简单的Promise来拉扯?本文讲解使用以及细节和手写思路。
正文
Promise是异步编程的一中解决方案,最早是由社区提出的,es6中正式的将其纳入,他是一个对象,可以获取到异步的操作,他相比传统的回调函数,更加的强大和合理,避免了回调地狱。所谓的Promise,简单的来说就是一个可以存放未来才能结束的任务或者事件...
promise有三种状态:pending ,fulfilled,rejected
-
pending -- fulfilled (进行到成功)
-
pending -- rejected (进行到失败)
常用的方法有5中:then()、catch()、all()、race()、finally()。”
基本使用
//先new 一个Promise实例对象,并且传入回调函数,resolve和reject作为形参
const promise = new Promise((resolve,reject)=>{
//根据时机执行resolve('success')或者reject('fail')
})
promise.then((res)=>{
console.log('res',res) //打印success
},(err)=>{
console.log('err',err) //打印fail
})
//第一个函数在resolve执行回调,第二个在reject执行回调
当我们在new Promise中传递一个函数进去,函数的形参接收两个参数resolve,reject。如果我们在函数体内执行了resolve(),实例对象就会由pending转为fulfilled状态,resolve()中的值作为返回值,并且then方法的第一个传参函数会被调用;如果执行了reject(),会由pending转为rejected,then方法的第二个传参函数会被调用。
resolve
- resolve的其他传值
- 传入正常值或者对象
- 传入一个promise对象
1 const promiseNew = new Promise((resolve,reject)=>{
2 //一个新的Promise,将作为resolve参数传入,其resolve的执行才会改变状态
3 })
4 const p =new Promise((resolve,reject)=>{
5 resolve(promiseNew) //resolve一个promise对象,
6 })
7 p.then((res)=>{
8 console.log(“res",res)
9 },err=>{
10 console.log(err)
11 })
执行后,会在我意料之中,第8行的打印根本不会执行。因为resolve(promiseNew)传入一个promise对象会接管当前p对象的pending状态,只有promiseNew的resolve才执行第p的状态改变为fulfilled,执行then回调。
-
- 传入一个实现thenable接口对象
Thenable接口一般出现在面向对象编程语言中,在ECMAScript暴露的异步结构中,对象有一个then()方法。这个方法实现了Thenable接口
注意: Thenable接口不是Promise专属的。Promise类型实现了thenable接口。 比如:
const obj = {
then:function(resolve,reject){//resolve,reject参数自动传递进来
resolve("resolve message")
}
}
new Promise((resolve,reject)=>{
resolve(obj) //传递res出去,
}).then((res)=>{
console.log(“res",res) //打印执行结果为:“resolve message”
},err=>{
console.log(err)
})
”
reject()
1.正常写法
const promise = new Promise((resolve,reject)=>{
reject("reject")
})
promise.then(()=>{},(err)=>{
console.log(err)
})
2.其他写法
const promise = new Promise((resolve,reject)=>{
throw new Error("reject")
})
promise.then(()=>{},(err)=>{
console.log(err)
})
处理一下then回调细节上的问题。如果then回调内return会咋样?”
then方法内的return
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(222);
}, 2000);
});
在Promise上使用return返回,等同于Promise.resolve(res),返回值将被用new Promise((resolve)=>{ resolve(res) })
p.then(res=>{
return "aaaa"
}).then(res=>{
console.log(res) //结果:aaaa
})
- return 普通类型如字符串
p.then(res=>{
return "aaaa"
}).then(res=>{
console.log(res) //结果:aaaa
})
- 没有返回值
没有返回值,默认返回Promise.resolve(undefined)
- 返回一个promise
p.then(res=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(111)
},3000)
})
}).then(res=>{
console.log(res +"promise") //111promise
},err=>{})
当返回一个promise对象,那么意味着下一个生成的promise的then方法被返回的对象续约(被它托管状态实现成功回调还是错误回调)
- 返回一个thenable接口的对象
promise.then(res=>{
return {
then:function(resolve,reject){
resolve(222)
}
}
}).then(res=>{
console.log(res +"then") //222then
},err=>{})
我们试想:错误该被谁捕获到?
const p = new Promise((resolve,reject)=>{
throw new Error("reject");
})
p.then(
(res2) => {
console.log(res2);
},
(err1) => {
console.log(err1 + "err1");
}
).catch((err2) => {
console.log(err2 + "err2");
});
catch优先捕获顶级promise的异常,如果没有就往下捕获。
finially方法
无论是reject还是fuililed状态,最终都会执行的方法
const promise = new Promise((resolve,reject)=>{
throw new Error("reject")
})
promise.then(()=>{},(err)=>{
console.log(err)
}).finally(()=>{
console.log("无论如何都会执行")
})
Promise.resolve
const promise = Promise.resolve({name:"mjc"})
promise2.then(res=>{
console.log(res) //{name:"mjc"}
})
//等价于
const promise2 = new Promise((resolve,reject)=>{
resolve({name:"mjc"})
}).then(res=>{
console.log(res) //{name:"mjc"}
})
Promise.resolve()直接返回一个promise实例,并且再then回调中回调参数。
Promise.reject
等同于以上的resolve调用,返回一个rejected状态的promise实例,不同的是reject根据传入不同的值没有关系,比如传什么就是什么,而不是会根据传入promise或者thenable对象而改变回调的时机。
Promise.all
适用场景(多个异步函数逐个执行比如axios请求)
const p1 = new Promise((resolve,reject)=>{
resolve(111)
})
const p2 = new Promise((resolve,reject)=>{
resolve(222)
})
const p3 = new Promise((resolve,reject)=>{
resolve(333)
})
//让所有的promise都状态位fulfilled时,再拿到结果
Promise.all([p1,p2,p3]).then(res=>{
console.log(res) //[111,222,333]
}).catch(err=>[
console.log(err)
])
//如果在之前,有一个promise变成了reject,那么整个promise是rejected
all()接收一个数组或者可迭代interator对象,数组内为promise实例。如果数组内某一个promise状态位rejected,则抛出错误被catch或者err第二个回调捕获,否则,全为fulfilled在then回调内执行并且接收返回的数组。
all方法有一个缺陷,如果存在resolve获取到了的数据,但是因为下个promise的rejected状态那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的(说白了,一个队友坑,全队都没得赢),所以我们需要对每个promise独立起来。那解决办法是ES11中的Api:allSettled
Promise.allSettled
const p1 = new Promise((resolve,reject)=>{
resolve(111)
})
const p2 = new Promise((resolve,reject)=>{
reject(222)
})
const p3 = new Promise((resolve,reject)=>{
resolve(333)
})
//让所有的promise都状态位fulfilled时,再拿到结果
Promise.allSettled([p1,p2,p3]).then(res=>{
console.log(res) //
}).catch(err=>[
console.log(err)
])
//无论有没有reject,都不会走catch方法
返回的结果是一个数组,里面是每个promise对应的结果分为status和value,status很好的解释了该promise的状态是成功的还是失败的,而value为resolve或者reject的结果。这样每个独立的promise不会因为某个的rejected状态而拿不到已获取的数据。
Promise.race
race理解为竞赛,可想而知和all结构一样,所有promise实例谁先异步达到了fuilfilled状态,那么then回调立刻执行返回结果。如果某实例先达到了rejected状态,就会执行catch而不会执行then()。说白了:
- 只要哪个promise优先resolve成为fulfilled状态,就可以立刻调用then方法。
- 只要哪个promise优先reject成为rejected状态,就可以立刻调用catch方法。
any方法是ES12中新增的方法,和race方法是类似的
Promise.any
const p1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(222)
},3000)
})
const p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(222)
},2000)
})
Promise.any([p1,p2]).then(res=>{
console.log(res) // 222
}).catch(err=>[
console.log(err)
])
any方法会等到一个fulfilled状态,才会决定新Promisel的状态执行then回调;如果所有的Promise都是rejected的,那么也会等到所有的Promise都变成rejected状态才抛出错误被catch捕获。(说白了,和race方法的不同是一个队友坑就算了,全部队友都坑才抛出异常)。
手写promis实现
从0开始实现一个promise的复制版本,以下不作类型和参数验证考虑。
01_基本骨架
promis的手写实现用到Es6中的class类声明,当然用构造函数也是一样的。promise基本骨架如下:
class MjcPromise {
constructor(executor) {
const resolve = (res) => {
console.log(res);
};
const reject = (err) => {};
executor && executor(resolve, reject);
}
}
const promise = new MPromise((resolve, reject) => {
resolve(666);
})
当我们new MjcPromise的时候,执行类的constructor构造器,接收一个函数执行体,同时并且执行将自定义的resolve,reject传递进去当做resolve,reject的变量接收。
02_实现简单状态管理
基于基本结构,我们需要给每个new 出来的实例一个状态。在原生promise中有fulfilled,rejected,pending状态改变。
const PROMISE_STATUS_PENDING = "pending";
const PROMISE_STATUS_FULFILLED = "fulfilled";
const PROMISE_STATUS_REJECTED = "rejected";
改变constructor构造器内部,默认为pending
this.status = PROMISE_STATUS_PENDING;
this.value = undefined;
this.reason = undefined;
改变resolve和rejected,当执行的时候修改status状态
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING)
{
this.status = PROMISE_STATUS_FULFILLED;
this.value = value;
console.log("status", this.status, "val ue", this.value);
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_REJECTED;
this.reason = reason;
console.log("status", this.status, "reason", this.reason);
}
}
const p = new MjcPromise((resolve, reject) => {
resolve("fulfilled状态");
//reject("rejected状态");
});
我们分别执行resolve,reject看看效果:
03_then函数的简单实现
constructor(executor) {
...
this.onFulfilled = undefined;//新增
this.onRejected = undefined;//新增
const resolve = (value) => {
...
this.onFulfilled(this.value);//新增
}
};
const reject = (reason) => {
...
this.onRejected(this.reason);//新增
}
};
}
then(onFulfilled, onRejected) {//新增
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
...
p.then(
(res) => {
console.log(res);
},
(err) => {
console.log(err);
}
);
constructor添加两个onFulfilled,onRejected用来存储then函数的回调。定义类方法then,接收两个回调,同时赋值。部分重复代码用...展示。
会发现resolve执行后会报错,那是因为new promise里的回调函数是同步执行的,当resolve的时候,p.then还没有执行,所以onFulfilled 为undefined。我们需要给resolve内部添加setTimeout或者queueMicrotask异步任务。异步任务不会阻塞代码,会进入到宏任务中。等到同步代码p.then执行完后再去执行resolve。
所以如下修改resolve和reject函数:
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDI NG) {
this.status = PROMISE_STATUS_FULFILLED;
queueMicrotask(() => {//新增
this.value = value;
this.onFulfilled(this.value);
});
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_REJECTED;
queueMicrotask(() => {//新增
this.reason = reason;
this.onRejected(this.reason);
});
}
};
至此,一个then回调完成了。
这就是promise简单的基本手写思路。在面试中写到这里已经基本差不多了。但是可以更加完全接近promise的所有功能。
03_then的优化升级
原生promise的then方法可以被多次调用,这有点像vue内的响应式收集。当我们调用多个then方法,会被收集到桶里面。
如果这样:
p.then(
(res) => {
console.log("res1", res);
},
(err) => {
console.log("err1", err);
}
);
p.then(
(res) => {
console.log("res2", res);
},
(err) => {
console.log("err2", err);
}
);
setTimeout(() => {
p.then(
(res3) => {
console.log(res3 + "res3");
},
(err3) => {
console.log(err3 + "err3");
}
);
}, 1000);
所以收集then回调的得是个可迭代的对象或者数组,在这里用数组代替。在resolve或者reject执行时对收集的数组内遍历函数
this.onFulfilled = [];//修改
this.onRejected = [];//修改
----------------------------
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
//this.status = PROMISE_STATUS_FULFILLED;//变动到微任务内
queueMicrotask(() => {
this.status = PROMISE_STATUS_FULFILLED;
this.value = value;
this.onFulfilled.forEach((fn) => { //修改
fn && fn(this.value);
});
});
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
//this.status = PROMISE_STATUS_REJECTED;//变动到微任务内
queueMicrotask(() => {
this.status = PROMISE_STATUS_REJECTED;
this.reason = reason;
this.onRejected.forEach((fn) => {//修改
fn && fn(this.reason);
});
});
}
};
同时then方法内也要修改,防止在全局异步下添加的then函数没有被执行,如果当前状态已经被修改了,说明queueMicrotask已经执行了,是全局setTimeout添加的then方法,就直接执行函数;如果仍然是pending,说明仍然在收集then回调。
then(onFulfilled, onRejected) {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {//新增
onFulfilled(this.value);
return
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {//新增
onRejected(this.reason);
return
}
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilled.push(onFulfilled);
this.onRejected.push(onRejected);
}
这里特别注意一点: resolve和reject函数内的queueMicrotask微任务前的this.status状态改变需要移入queueMicrotask内部判断;不然会出现如下undefined效果:
原因是在新增的then方法内做了状态判断,在改变状态后执行为微任务队列,p.then方法会直接执行当前回调,所以this.value是undefined,我们需要将状态改变放到微任务队列中。
如何实现then的链式调用?我们知道then会返回一个promise对象,所以then需要return一个出去
then(onFulfilled, onRejected) {
return new MjcPromise((resolve, reject) => {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
onFulfilled(this.value);
return;
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
onRejected(this.reason);
return;
}
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilled.push(onFulfilled);
this.onRejected.push(onRejected);
}
});
}
最麻烦的问题来了,当前return 出去的promise是需要resolve或者reject的,而我们必须得拿到上一次结果的返回值,在新promise内resolve()出去。我们需要改变this.onFulfilled//onRejected的push对象,拿到返回值在这里resolve。
then(onFulfilled, onRejected) {
return new MjcPromise((resolve, reject) => {
...
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilled.push(() => {
let value = onFulfilled(this.value);
resolve(value);
});
this.onRejected.push(() => {
let value = onRejected(this.reason);
resolve(value);
});
}
});
}
当用箭头函数的作用可以通过this的不绑定原则能拿到实例上的值。
p.then(
(res) => {
console.log("res1", res);
return "mjc";
},
(err) => {
console.log("err1", err);
return "mjcerr";
}
).then(
(res) => {
console.log("res2", res);
},
(err) => {
console.log("err2", err);
}
);
一开始这种处理确实难理解,因为没看懂this和resolve代表哪个promise。我们只要清楚this指向就知道如何执行的。如图:
我们在promise内传递的函数是箭头函数,那就意味着this指向上下文,也就是说this是指向上一个promise实例,而resolve的调用是处理当前new MjcPromise对象的状态。
至此已经基本实现链式调用了。事实上剩下的是错误不断处理,判断类型正确,因为篇幅有限不展开了。 以上部分思路参考promiseA+以及coderwhy大神技术参考。
总结
- promise使用以及链式调用
- promise的resolve()
- promise的then方法return细节
- Promise类方法使用
- Promise的基本手写实现
人会有遗忘,文字不会遗忘记录生活,记录一切,贯彻落实终身学习观念先定个小目标:先写五年博客祝我们的实力像for循环的i++一样,越来越强~
参考资料
[1] coderwhy ke.qq.com/course/pack…
[2] Promises/A+ promisesaplus.com
[2] 博客blog mjcelaine.top