一文,一定能让你手动实现Promise

807 阅读8分钟

Promise(中文意思:承诺)对象是ES6带来的一个新的异步编程的解决方案,与传统的回调函数更加合理强大。

概念

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。

状态

一个 Promise有以下几种状态:

  • pending:初始的状态。
  • fulfilled:成功。
  • rejected:失败。


(上面都讲了啥啊?半句没有听懂,磨刀中...ing)

举个栗

(各位大哥不要着急......是我错了,下面这个例子包你懂)

假设我有一个女朋友(好吧,我没有,程序猿是不可能有女朋友的!!!),她明天生日,今天中午我答应明天肯定帮她庆生给她惊喜,那么我答应她的这个承诺就开始进入等待状态(pending),等待着明天的到来。如果明天我按时陪她吃饭给她礼物惊喜,完成了这个承诺,那这个承诺的状态就由等待状态变成了成功完成状态(fulfilled),这个承诺一旦完成了就有了结果了,那就不会有其他状态了,也就是承诺的状态不会再改变了;但如果明天我工作太忙要加班忘记了,没有完成这个承诺,那状态也就由等待状态变成了失败状态(rejected),状态也不会再有改变了(基本女朋友也没有了...)。

Promise大致就是干了怎么一件事件,懂了撒?

基本用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

基本用法一

let p = new Promise((resolve, reject) => {
  setTimeout(() => { // 异步操作
    if(true) { // 成功
      resolve('我是异步返回的成功数据');
    }else { // 失败
      reject('我是异步返回的失败数据');
    }
  }, 1000)
  console.log('我先输入'); // 构造函数立刻执行
})

p.then(res => {
  console.log(res); // 我是异步返回的成功数据
}, err => {
  console.log(err); //我是异步返回的失败数据
})

基本用法二

改用了.catch来捕获异常,这估计才是比较常用的方式吧?反正小编更偏向这种写法。。。

let p = new Promise((resolve, reject) => {
  setTimeout(() => { // 异步操作
    if(true) { // 成功
      resolve('我是异步返回的成功数据');
    }else { // 失败
      reject('我是异步返回的失败数据');
    }
  }, 1000)
  console.log('我先输入'); // 构造函数立刻执行
})

p.then(res => {
  console.log(res); // 我是异步返回的成功数据
}).catch(err => {
  console.log(err); //我是异步返回的失败数据
})

实现手动Promise

同步手写版(面试手写够用^-^)

简单回忆了下基本用法,下面我们就赶紧步入正题。

我们以构造函数的形式来搞它吧,有大佬也有用ES6的Class的形式来实现的,感兴趣的小伙伴可以自行某度看看。

首先把Promise构造函数定义出来。

function myPromise() {
	
}

观察上面基本用法,能知道构造函数接收一个函数作为参数,并且该函数还是立刻执行的(console.log('我先输入'); // 构造函数立刻执行),且函数还接收另外两个回调函数作为参数(Promise对象其实本质还是利用回调函数的语法糖来工作)。

function myPromise(constructor) {
  function resolve() {

  }
  function reject() {

  }
  constructor(resolve, reject);
}

然后我们看到.then()方法,它能通过构造函数的实例访问到,很明显我们得在构造函数的原型上创建这个方法才行(原型?请点),且它接收两个函数(看上面 基本用法一.then()调用),也就是成功回调函数与失败回调函数。

myPromise.prototype.then = function(fulfilled, rejected){
    
}

然后我们来继续完善 myPromise() 这个构造函数,前面我们说过Promise对象有三种状态,通过执行不同的回调(resolve()reject())来改变这个Promise的状态。

function myPromise(constructor) {
  let _this = this; // 保留原来this, 防止指向不明
  _this.status = 'pending'; // 初始状态

  function resolve() {
    _this.status = 'fulfilled'; // 成功状态
  }
  function reject() {
    _this.status = 'rejected'; // 失败状态
  }
  constructor(resolve, reject);
}

当然,我们在上面那个举例中就说明了,一旦状态改变了就不能再改变了,所以我们还需要在成功与失败回调中做一些判断,保证它们只做一次。

function myPromise(constructor) {
  let _this = this; // 保留原来this, 防止指向不明
  _this.status = 'pending'; // 初始状态
  
  function resolve() {
    if(_this.status === 'pending') {
      _this.status = 'fulfilled'; // 成功状态
    }
  }
  function reject() {
    if(_this.status === 'pending') {
      _this.status = 'rejected'; // 失败状态
    }
  }
  constructor(resolve, reject);
}

我们再看到.then()方法中是能接收到结果的,而我们的结果是在回调(resolve('我是异步返回的成功数据');)中传递进去的,所以我们要在成功与失败的回调中把结果也给保存一下,保证在调用.then()方法的时候有结果返回。

function myPromise(constructor) {
  let _this = this; // 保留原来this, 防止指向不明
  _this.status = 'pending'; // 初始状态
  _this.result = undefined; // 保存成功的结果
  _this.reason = undefined; // 保存失败的结果
  
  function resolve(result) {
    if(_this.status === 'pending') {
      _this.status = 'fulfilled'; // 成功状态
      _this.result = result;
    }
  }
  function reject(reason) {
    if(_this.status === 'pending') {
      _this.status = 'rejected'; // 失败状态
      _this.reason = reason;
    }
  }
  constructor(resolve, reject);
}

保存了结果, myPromise() 构造方法做的事就差不多完了,剩下.then()方法,它需要做的只有一件事情,合理的返回对应的结果。

myPromise.prototype.then = function(fulfilled, rejected) {
  let _this = this; // 该this与上方提的this是一致的,不懂的小伙伴要去看看原型链相关的内容哦。
  switch(_this.status) {
    case 'fulfilled': 
        fulfilled(_this.result);
        break;
    case 'rejected':
        rejected(_this.reason);
        break;
  }
}

到此为止,Promise对象简单的结构基本就实现了,我们简单测试一下。

let p = new myPromise((resolve, reject) => {
  if(true) { // 成功
    resolve('成功数据');
  }else { // 失败
    reject('失败数据');
  }
  console.log('我先输入'); // 构造函数立刻执行
})

p.then(res => {
  console.log(res); // 成功数据
}, err => {
  console.log(err); //失败数据
})

这就完了吗? 当然没有,这只是同步处理的实现。

异步手写版

首先我们知道JS是个单线程,执行顺序是从上到下顺序执行,如果遇到异步任务就会先丢到一边继续往下执行,之后再回头,异步任务又可以分为宏任务与微任务,具体是个什么样子的概念就不展开说了(点它点它),反正记住如果是异步操作,那异步下面的代码肯定比它先执行,这要注意注意!!!

回到正题,看我们上面的 基本用法 里面都是含着一个异步操作,.then()方法调用的时候,实际我们还并未调用回调函数(resolve()reject()都处于异步操作中,不会先执行)去改变Promise对象的状态,所以Promise对象的状态还是pending状态,此时我们并不知道执行.then()方法中的成功回调函数还是失败回调函数,也没有相关的结果可以返回。所以我需要先将成功回调函数还是失败回调函数保存起来,等待合适的时机再执行调用。

在 myPromise() 构造函数上创建个保存回调函数的地方,需要注意Promise对象支持链式调用,所以可能会连续调用多个.then()方法,所以可能会有多组成功回调函数与失败回调函数。

function myPromise(constructor) {
  let _this = this; // 保留原来this, 防止指向不明
  _this.status = 'pending'; // 初始状态
  _this.result = undefined; // 保存成功的结果
  _this.reason = undefined; // 保存失败的结果
  _this.fulfilledCallbacks = []; // 存放成功的回调
  _this.rejectedCallbacks = []; // 存放失败的回调

  function resolve(result) {
    if(_this.status === 'pending') {
      _this.status = 'fulfilled'; // 成功状态
      _this.result = result;
      _this.fulfilledCallbacks.forEach(fulfilledCb => {
        fulfilledCb();
      });
    }
  }
  function reject(reason) {
    if(_this.status === 'pending') {
      _this.status = 'rejected'; // 失败状态
      _this.reason = reason;
      _this.rejectedCallbacks.forEach(rejectedCb => {
        rejectedCb();
      });
    }
  }
  constructor(resolve, reject);
}

.then()将成功回调函数与失败回调函数保存起来等待合适的时机执行调用。

myPromise.prototype.then = function(fulfilled, rejected) {
  let _this = this; // 该this与上方提的this是一致的,不懂的小伙伴要去看看原型链相关的内容哦。
  switch(_this.status) {
    case 'fulfilled':
      fulfilled(_this.result);
      break;
    case 'rejected':
      rejected(_this.reason);
      break;
    case 'pending':
      _this.fulfilledCallbacks.push(() => {
        fulfilled(_this.result);
      });
      _this.rejectedCallbacks.push(() => {
        rejected(_this.reason);
      });
      break;
  }
}

测试代码(也就差不多是上面 基本用法 的代码了)

let p = new myPromise((resolve, reject) => {
  setTimeout(() => { // 异步操作
    if(false) { // 成功
      resolve('我是异步返回的成功数据');
    }else { // 失败
      reject('我是异步返回的失败数据');
    }
  }, 1000)
  console.log('我先输入'); // 构造函数立刻执行
})

p.then(res => {
  console.log(res); // 我是异步返回的成功数据
}, err => {
  console.log(err); //我是异步返回的失败数据
})

这就完了吗? 怎么问,肯定又还没有咯。

我们让Promise对象抛出一个错误看看。

let p = new myPromise((resolve, reject) => {
  throw new Error('出错了')
})

p.then(res => {
  console.log('res:' + res); // 我是异步返回的成功数据
}, err => {
  console.log('err:' + err); //我是异步返回的失败数据
})

这种错误好像没有进入我们预想的失败回调函数一样,我们预想应该这种错误也要进入失败回调函数才是正常。

解决这种错误。

try{
  constructor(resolve, reject);
}catch(e) {
  reject(e);
}

这就好了,很完美,不错了,你已经很棒了。

完整源码

function myPromise(constructor) {
  let _this = this; // 保留原来this, 防止指向不明
  _this.status = 'pending'; // 初始状态
  _this.result = undefined; // 保存成功的结果
  _this.reason = undefined; // 保存失败的结果
  _this.fulfilledCallbacks = []; // 存放成功的回调
  _this.rejectedCallbacks = []; // 存放失败的回调

  function resolve(result) {
    if(_this.status === 'pending') {
      _this.status = 'fulfilled'; // 成功状态
      _this.result = result;
      _this.fulfilledCallbacks.forEach(fulfilledCb => {
        fulfilledCb();
      });
    }
  }
  function reject(reason) {
    if(_this.status === 'pending') {
      _this.status = 'rejected'; // 失败状态
      _this.reason = reason;
      _this.rejectedCallbacks.forEach(rejectedCb => {
        rejectedCb();
      });
    }
  }
  constructor(resolve, reject);
}


myPromise.prototype.then = function(fulfilled, rejected) {
  let _this = this; // 该this与上方提的this是一致的,不懂的小伙伴要去看看原型链相关的内容哦。
  switch(_this.status) {
    case 'fulfilled':
      fulfilled(_this.result);
      break;
    case 'rejected':
      rejected(_this.reason);
      break;
    case 'pending':
      _this.fulfilledCallbacks.push(() => {
        fulfilled(_this.result);
      });
      _this.rejectedCallbacks.push(() => {
        rejected(_this.reason);
      });
      break;
  }
}

(嘿嘿,其实,还有的一点内容的,我们上面不是说过.then()支持链式调用吗?呃......就留个悬念吧?如果后面有人要看再补上吧,哈哈,拜拜~)