要学会用Promise拉扯一手细节

844 阅读11分钟

前言


大家好,我是麦当当,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的其他传值
    • 传入正常值或者对象
    这个没啥好说,then回调正常获取
    • 传入一个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看看效果:

01.gif

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大神技术参考。

总结

  1. promise使用以及链式调用
  2. promise的resolve()
  3. promise的then方法return细节
  4. Promise类方法使用
  5. Promise的基本手写实现

人会有遗忘,文字不会遗忘记录生活,记录一切,贯彻落实终身学习观念先定个小目标:先写五年博客祝我们的实力像for循环的i++一样,越来越强~

参考资料

[1] coderwhy ke.qq.com/course/pack…

[2] Promises/A+ promisesaplus.com

[2] 博客blog mjcelaine.top