手写Promise源码几乎是每个大厂面试要求必会的一个考点,每次听到源码,总有一种让人上头的感觉,因为自己从来没有实现过,总感觉这东西很难实现,最近再为跳槽做准备,从头写学了一下基础知识,手写了call源码、apply源码、Promise源码,感觉还挺有意思,不是想想的那么难。就是一个js的简答实现。只要优点js基础的人都能手写出来,所以不要一听“源码”二字就给吓到。自己动手实现一遍,比看别人的几十遍的效果更好。
本篇文章从实际应用角度思考Promise是怎样的一个实现过程,会先从简单的应用出发,然后一点一点去完善整个代码。
先来一个简单的例子:
//刚开始是等待态,pending
let promise = new Promise( (resolve,reject) =>{if(err) return reject(err) //失败了返回失败信息 失败态
resolve(data) //成功了返回数据 成功态
})
//状态改变了调用
promise.then(data=>{ //成功了调用
console.log(data)
},err=>{ //失败了调用
console.log(err)
})
这是一个promise实例,它有三种状态,pending(等待态)、fullfilled(成功态)、rejected(失败态),resolve为成功时调用,状态由等待态变为成功态,reject为失败,状态由等待态变为失败态。
当状态改变的了的时候会执行then方法,如果是成功就输出值成功的值,如果是失败就返回失败的原因。但是他们两个只能调用一个,不可能有即成功由失败的情况。
根据这个实例我们就可以实现一版简单的功能。
第一版:定义整个流程,实现同步功能:
这一版我们主要实现流程能走通,能执行同步任务
class MyPromise{
constructor(executorCallback){ //executorCallback执行的回调函数
let _this = this;
_this.status = 'pending'; //记录状态
_this.value = ''; //记录返回的数据
function resolveFn(result){
if(_this.status === 'pending'){ //状态只能是从pending到成功
_this.status = 'resolved'
_this.value = result
}
}
function rejectFn(reason){
if(_this.status === 'pending'){ //状态只能是从pending到失败
_this.status = 'rejected'
_this.value= reason
}
}
executorCallback(resolveFn,rejectFn)
}
//判断状态成功还是失败,成功了执行onFullfiled,失败了执行onRejected
then(onFullfiled,onRejected) {
if(this.status === 'resolved'){
onFullfiled(this.data)
}
if(this.status === 'rejected'){
onRejected(this.err)
}
}
}
1.定义_this保证每次都能获取到正确的this
2.定义一个常量status用来记录promise的状态
3.定义一个常量value用来保存执行结果的返回值
4.executorCallback是立即执行函数,需要我们手动传入两个参数,这两个参数代表成功和失败时候调用,需要我们在内部定义好。
5.resolveFn()和rejectedFn()函数,在执行前先判断一下状态,如果状态为pending就执行,并且让状态变为相应的成功态或者失败态,这样每次就只能执行一个,要么是resolveFn(),
要么是rejectedFn(),并且把相应的返回值赋值给value。
6.状态改变了之后就会调用Promise.then()方法,如果状态是成功态就执行onFullfilled(),如果状态是失败态,就执行onRejected()
到此基本流程通了,我们写个小案例测一下
可以看到,虽然我写了两个函数,但是它只执行了第一个,就说明当状态pending变为成功态之后不会再去执行失败的函数,同理当状态变为失败态之后也不会再去执行成功的函数,
现在虽然实现了可以执行同步任务,但是对于异步任务还是执行不了。例如
可以看到控制台没有输出任何东西,现在我们就来解决一下如何实现异步任务。
第二版:实现异步功能
我们先来分析一下上一版为什么实现不了异步功能,当代码执行到setTimeout时,不会立即执行里面的函数,而是先放到一个异步调用栈里面,等到同步代码执行完了在执行里面的resolveFn函数,这个时候then已经执行完了,不会再执行,所以就不会输出任何东西,我们可以在resolveFn执行之前就将then的回调函数先保存起来,等到resolveFn执行的时候再去一个一个执行这些回调函数,这个时候就可以实现异步功能。
在原来的基础上修改
class myPromise{
constructor(executorCallback){
var _this = this
_this.status = 'pending'
_this.value
_this.onFullfilledCallback = [] //存放成功时的回调函数
_this.onRejectedCallback = [] //存放时的失败的回调函数
function resolveFn (result) {
let timer = setTimeout( () =>{ //异步任务
clearTimeout(timer)
// console.log('chengg')
if(_this.status === 'pending'){
_this.status = 'resolved'
_this.value = result
_this.onFullfilledCallback .forEach(item =>item(_this.value));
}
})
}
function rejectFn (err) {
let timer = setTimeout( () =>{ //异步任务
clearTimeout(timer)
if(_this.pending === 'pending'){
_this.status = 'rejected'
_this.value = err
_this.onRejectedCallback.forEach(item =>item(_this.value))
}
})
}
executorCallback(resolveFn,rejectFn)
}
then(onFullfiled,onRejected){
if(this.status === 'pending') {
this.onFullfilledCallback .push(onFullfiled)
this.onRejectedCallback.push(onRejected)
}
}
}
我将修改了的部分用红色字体标出。
1.onResolvedCallback和onRejectedCallback用来存放成功和失败时候的回调函数,想想为什么是一个数组呢?我们前面分析过,第一个原因是同一个Promise实例可以调用多次then,
需要把这些方法都放在同一个数组里,例如
let p1 = new Promise( (resolve,reject) =>{
setTimeout(function(){
resolve('ok')
},1000)
})
p1.then(result =>{
console.log('result1:'+result)
},reason =>{
console.log(reason)
})
p1.then(result =>{
console.log('result2:'+result)
},reason =>{
console.log(reason)
})
第二个原因是当立即执行完 Promise 时,让它的状态还是pending的时候,应该把 then 中的回调保存起来,当执行成功或者失败,状态改变时再执行
2.resolveFn()和rejected()这里为什么要用setTimeout将它变为异步执行呢?因为如果不用setTimeou这种方式的话,若Promise里面的代码是同步代码,在执行到reject或者resolve的时候,还没有执行then,所以数组里还没有值,这个时候调用的话不会报错但是不会输出任何结果,用setTimeout转为异步的话,会先去执行then方法,将回调收集到数组里,然后再去执行异步任务,这个时候就有值了。举例子:
此时红色方框内的是同步代码,会先执行,不会输出任何东西,当然如果把红色方框内的变为异步代码就不会有这个问题了。但是我们要同时兼顾同步和异步都存在的情况。
3.then方法:
then(onFullfiled,onRejected){
if(this.status === 'pending') {
this.onFullfilledCallback .push(onFullfiled)
this.onRejectedCallback.push(onRejected)
}
}
then很简单,就是在状态为pending的时候将回调函数收集到数组里面,到此异步功能就差不多了,我们写个例子试一下。
let p1 = new myPromise((resolve,reject) =>{
setTimeout(function(){
resolve('第一次成功')
},1000)
})
p1.then(result =>{
console.log('result:'+result)
},reason =>{
console.log('reason:'+reason)
})
完美。
但是这个第二版还不能实现链式调用,在工作中我们经常通过promis.then().then()这样的方式来解决回调地狱,如下图。
接下来我们就实现链式调用
第三版:实现链式调用
首先分析,Promise为什么可以实现链式调用,因为Promise.then()方法它返回的是一个新的Promise实例,将这个新的Promise实例的返回值传递到下一个then中,作为下次onFullfilled()或者onRejected()的值。那这个新的Promise的返回值会有哪几种情况呢?我们来分析一下
then(onFulfiled,onRejected){
// 声明返回的promise2
let promise2 = new myPromise((resolveFn, rejectFn)=>{
if (this.status === 'fulfilled') {
let x = onFulfiled(this.value);
// resolvePromise函数,处理自己return的promise和默认的promise2的关系
resolvePromise(promise2, x, resolveFn, rejectFn);
};
if (this.status === 'rejected') {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolveFn, rejectFn);
};
if (this.status === 'pending') {
this.onFullfilledCallback.push(()=>{
let x = onFulfiled(this.value);
resolvePromise(promise2, x, resolveFn, rejectFn);
})
this.onRejectedCallback.push(()=>{
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolveFn, rejectFn);
})
}
});
// 返回promise,完成链式
return promise2;
}
我们首先定义一个新的Promise实例,在这个实例内部需要判断状态,当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数。将then回调函数的返回值记为x,由于这个返回值可能有多种情况,所以需要对各种情况进行解析。所以另外封装一个方法resolvePromise(),接下来我们就来封装一下这个函数。
function resolvePromise(promise2, x, resolve, reject){
// 循环引用报错
if(x === promise2){
// reject报错
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called; //控制调用次数
// x不是null 且x是对象或者函数
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
// PromiseA+规定,声明then 等于 x的then方法
let then = x.then;
// 如果then是函数,就默认是promise了
if (typeof then === 'function') {
// 就让then执行 第一个参数是this, 后面是成功的回调 和 失败的回调
then.call(x, y => {
// 成功和失败只能调用一个
if (called) return;
called = true;
// resolve的结果依旧是promise 那就继续解析
resolvePromise(promise2, y, resolve, reject);
}, err => {
// 成功和失败只能调用一个
if (called) return;
called = true;
reject(err);
})
} else { //如果不是函数,是普通对象直接resolve
resolve(x); // 直接成功即可
}
} catch (e) {
// 如果在执行的过程中报错了,就被then的失败捕获
if (called) return;
called = true; // 取then出错了那就不要在继续执行了
reject(e);
}
} else { //如果是普通值
resolve(x);
}
}
1.PromiseA+规定 x 不能与 promise2 相等,这样会发生循环引用的问题
2.定义一个called来控制调用次数,成功和失败只能调用一个,一旦调用完就将called = true,防止下一个再调用。
3.接着判断x的类型,如果不是对象或者函数,那就是普通值,直接resolve(x)
4.如果是对象或者函数,将x.then赋值给then,如果then是函数,就让then执行回调函数。如果下一次的执行结果还是一个Promise,就接着处理。如果then是个普通对象,就直接执行
resolve方法
5.再执行这段代码的过程中可能会发生异常,我们用try catch去捕获错误,如有错误,就直接执行reject方法。
来测试一下是否可以实现链式调用了:
没毛病。
到此,Promise的核心功能都已经完成了,Promise还有一些其他的方法,all、race、resolve、reject,相信理解了上面的封装流程,大概就知道怎么封装了,当然前提是要知道这些方法的用法。接下来就看一下这些方法的实现。
Promise其他方法的实现
简单说一下,all方法接收一个数组,等到数组里面的所有Promise实例的状态都变成成功态之后才成功,但只要有一个失败了就返回失败。race方法是哪个先成功就先返回哪个,resolve和reject是分别执行成功和是失败。代码如下
//all 获取所有的promise,都执行then,把结果放到数组,一起返回 static all(promiseArr =[]) { return new Promise((resolve,reject) =>{ let index = 0; let arr = [] for(let i =0; i<promiseArr.length; i++){ promiseArr[i].then(result =>{ index++ arr[i] = result if(index === arr.length){ resolve(arr) } },reason =>{ reject(reason) }) } }) } //谁先执行先返回谁 static race (promises){ return new Promise((resolve,reject)=>{ for(let i=0;i<promises.length;i++){ promises[i].then(resolve,reject) }; }) } //resolve方法 static resolve(result){ return new Promise((resolve,reject)=>{ resolve(result) }); } //reject方法 static reject (reason){ return new Promise((resolve,reject)=>{ reject(reason) }); }
测试:
Promise源码就全部已经完成。阅读源码对我们思维能力会有很大的提升的,希望大家不要都可以动手实现一下,相信会有不少收获。