深挖一下Promise

149 阅读15分钟

每篇一句,送给爱学习的你

学了东西总归要记下来的,等老了以后再回过头来看很有意思,就像你现在打开朋友圈,去看三年前自己发的朋友圈,总会觉得自己当时好幼稚啊,哈哈哈,怎么会发这么傻的朋友圈,三年后再看自己写的文章,哈哈哈,自己当时好菜啊,这么多理解错误的地方。

今天的介绍对象Promise

Promise(期约),一个表示将来的动作,因为js是单线程,但是Promise表示将来才发生的事情,js不会等着它有结果了,再去执行后面的代码,所以呢,这里就会产生异步执行这样一种操作。等Promise有结果了,我们再来执行它里面定义的回调。而我们的Promise呢,也是ES6里面提出的一种,异步编程新的解决方案。

优点

  1. 可以链式调用,解决回调地狱问题
  2. 支持定义多个回调
  3. 支持异常穿透,只需要定义一个catch就可以捕获链式调用中任何阶段抛出的错误
  4. 支持回调函数的后定义。(先执行Promise,再给Promise的返回值定义回调函数,不像传统的异步回调,必须在写异步方法的时候,就要定义好回调)
  5. 想不到拉,剩下的优点谷歌一下吧。 缺点
  6. Promise执行进度无法追踪,处于pending状态不知道是程序刚开始,还是快结束了
  7. 一旦开始无法取消(后面会介绍一种方法,可以中断Promise的链式调用)
  8. Promise里面的错误如果不定义回调,无法反映到外部

今天要学习的重点有以下这些

  1. promise的状态:promiseState
  2. promise的结果:promiseResult
  3. promise原型上的方法:then,catch
  4. Promise私有方法:resolve,reject,all,race
  5. Promise的参数是个立即执行函数,也就是同步方法,但是then方法是异步的,属于微任务(宏任务,微任务也算是js事件循环中的重点吧,这块我有空的时候,也写篇文章记录一下,最近真的太忙拉,每天看react源码到12点,眼睛通红,为了生活还得继续啊)

备注:

  • 小写的promise是Promise类构造出来的实例,而大写的Promise呢是构造函数,大家要分清哦。
  • 立即执行函数,就是new Promise时,传给Promise的参数

promiseState

介绍

promiseState就是用来表示用Promise构造出来的实例的状态,当这个状态发生变化的时候,就会调用相应的回调函数(成功的回调或者失败的回调),这俩个回调函数是通过then方法去绑定的。

状态的取值有三种:

  1. pending:表示的是初始状态
  2. fulfilled/resolved:表示已成功(这俩个状态都表示一个意思已成功,后面我就用fulfilled来举例)
  3. rejected: 表示已失败(内部抛出错误也是这个状态)

状态的变化有俩种:

  1. 从pending状态变成fulfilled
  2. 从pending状态变成rejected

注意:状态的变化是不可逆的,一旦状态发生变化,该实例对象的状态将不再改变。也就是不会发生从fulfilled变成rejected,也不会从rejected变成fulfilled。在代码实现上就如下代码片段

if(promiseState !== 'pending) return;

promiseResult

介绍

promiseResult是用来表示promise实例的结果,这个结果是由Promise构造函数内部的resolve方法或者reject方法进行赋值的。promiseResult的值会被作为实参传递给回调函数中,当调用resolve方法,那么promiseResult就会被当做成功的回调参数,让抛出错误或者调用reject方法,那么promiseResult就会被当做失败的回调参数。

promiseResult取值有四种方式

  1. 当为初始状态的时候,promiseResult的值为undefined
  2. 当调用resolve方法的时候,resolve方法的参数即为promiseResult的值
  3. 当调用reject方法的时候,reject方法的参数即为promiseResult的值
  4. 当立即执行函数抛出错误的时候,抛出的错误对象,即为promiseResult的值

then,catch

介绍

then方法是给promise实例注册回调函数用的,then方法传入1或者2个函数作为参数(当然不传也是可以的,但是不传没有任何意义,这里就不做过多解释),第一个参数作为成功的回调函数,第二个参数作为失败的回调,并且then方法的返回值也是一个promise对象,这也是Promise支持链式调用的核心,因为返回的是promise对象,所以它就有then方法,所以它就可以继续调用then方法。(哈哈哈,一想到这里就想到了俄罗斯套娃,虽然这更像是接火车)。当为同一个promise实例注册多个then方法,这些then方法都会按顺序依次执行,不会是后面的then方法覆盖前面的then方法。

当然then方法最重要是还是里面的回调函数,因为回调函数的返回值可能是一个js的正常数据类型,但是它也可能是一个Promise对象。

  1. 当执行的是成功的回调函数时
  • 返回值为正常js数据类型,那么它就直接作为then方法返回的promise实例的promiseResult的值(哎,语文没学好,这句话怎么写都感觉不得劲。。。),并且状态为成功
  • 如果回调函数的返回值是一个promise对象,那么该promise对象返回的状态和结果即作为then函数返回的状态和结果 这里描述的有点复杂,但是代码其实并不复杂
  1. 当执行的是失败的回调函数时
  • 返回值是正常js数据类型,那么它状态为失败,结果为返回值
  • 返回值是promise对象,那么它的状态还是失败,值为promise对象
// onResolved方法就是传入then方法的成功的回调
// 这里的resolve和reject方法是Promise立即执行函数里面的参数
result = onResolved(this.promiseResult)
if(result instanceof Promise){
  // 回调函数返回值是promise对象,则根据promise对象的then方法来确定then方法的返回值类型
  result.then((v) => {
    resolve(v);
  }, (r) => {
    reject(r);
  })
} else {
  // 回调函数不是promise对象,则直接返回成功状态的promise对象
  resolve(result);
}

catch方法是给promise实例对象注册失败回调函数的地方,它只接收一个函数作为参数,这个函数就是失败的回调函数,失败有俩种情况。

  1. 调用了reject方法
  2. 是执行过程中抛出了错误

这两种情况都会在catch方法中的回调函数中进行处理,上面说的异常穿透,指的就是在链式调用过程中,只需要在链的最后边,加上一个catch方法,它就能捕获链中任意位置传报错,从而执行错误回调。catch的方法参数也比较简单,它和then方法的第二个回调函数基本上是一样的。

私有方法resolve,reject

介绍

这里的resolve和reject方法是Promise构造函数的私有方法,区别于promise实例对象原型上的resolve和reject方法,这是的resolve和reject方法是用来快速生成一个成功或失败的promise对象的。而原型上的方法是用来改变promise实例的状态和结果的。

resolve方法生成一个成功状态的promise对象,状态为成功。也就是promiseState='fulfilled',promiseResult='成功',reject方法也是类似的,生成一个状态为失败的promise对象。

const promise = Promise.resolve('成功');

私有方法all,race

介绍

all方法,接收一个由promise对象组成的数组,all方法的返回值也是一个promise对象。返回值有两种场景

  • 当数组中的promise对象全都返回成功的promise对象时,all方法返回的promise对象,状态为成功,结果为所有promise对象返回结果组成的数组,并且这个结果的顺序要和传入的promise对象数组的顺序保持一致。
  • 当数组中的promise对象,有一些返回的是失败的promise对象,则all方法返回的是第一个状态改变为失败的promise对象。

race方法,接收一个由promise对象组成的数组,race方法的返回值也是一个promise对象。race有赛跑的意思,所以race的返回值,就是数组中第一个改变状态的promise对象。

立即执行函数excutor

介绍

excutor为传入Promise的参数,它是个函数,有俩个方法resolve和reject,为什么叫立即执行函数呢?这是因为在js执行的时候,当执行到new Promise的时候,这个参数也就是这个函数会同步的被执行,所以叫它立即执行函数,这个在js的事件循环(eventLoop)中也是一个重点,只要考到事件循环,必有Promise的立即执行函数,then方法。所以这里也单独拎出来介绍一下这个立即执行函数

// 立即执行函数
let excutor = (resolve, reject) => {
    resolve('OK');
}
// 创建promise实例对象p
let p = new Promise(excutor);

知识点讲完啦

哈哈哈,是不是还浑浑噩噩的没听懂?没关系,接下来我就来手写实现一下这个Promise,以上知识都是脑子里面记住的哦,我可没有去翻笔记,去粘贴复制过来的哈。如果你也能手写Promise的话,那么恭喜你这个知识点,你已经熟练掌握啦。手写实现上面所有的函数,我大概需要20-30分钟。不知道你们花多长时间。哈哈哈,评论区比比看谁效率高(咳咳,咱不说快,以后男孩子们要记住说效率高)

手写Promise涉及的知识点

  1. 函数的异步执行
  2. 原型原型链
  3. this的指向和箭头函数

知识点不是很多,所以手写实现的话也比较简单。这就看基本功扎不扎实了。

手写实现Promise和它的函数们

介绍

知道每个函数是干什么的,和它的特性,手写就比较简单拉,我这边就使用ES6+语法了哈,现在不会还有人会写var了吧,var估计也只能出现出现在面试题里面去为难为难别人了。下面是编码时间(show time)。

Promise函数

// 定义常量,要不然下面拼写出错了,很难找出原因
// 很多大牛往往也会因为拼写出错而调试很久。哈哈哈
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected'

// 创建Promise构造函数,当然使用类也是一样的哈,本质上没什么区别
function Promise(excutor) {

  this.promiseState = PENDING;
  this.promiseResult = undefined;
  // 缓存then方法的回调
  this.callbacks = [];

  // 为resolve和reject函数传递外部的this进行使用
  // 当然这里使用箭头函数也是可以的,就不用传递this拉
  const self = this;

  // 成功时改变状态和结果的方法
  function resolve(data) {
    if (self.promiseState !== PENDING) return;
    self.promiseState = FULFILLED;
    self.promiseResult = data;
    // 下面的将缓存的回调,在状态改变之后进行调用
    // 模仿一下微任务
    setTimeout(() => {
      self.callbacks.forEach(item => {
        const {onResolved} = item;
        onResolved(data);
      });
    });
  }

  // 失败时改变状态和结果的方法
  function reject(data) {
    if (self.promiseState !== PENDING) return;
    self.promiseState = REJECTED;
    self.promiseResult = data;
    // 下面的将缓存的回调,在状态改变之后进行调用
    // 模仿一下微任务
    setTimeout(() => {
      self.callbacks.forEach(item => {
        const {onRejected} = item;
        onRejected(data);
      });
    });
  }

  // 这里是处理立即执行函数,如果执行抛出错误,直接将promise对象的状态改成rejected
  try {
    excutor(resolve, reject)
  } catch (error) {
    reject(error);
  }
}

// 定义原型上的then方法
Promise.prototype.then = function(onResolved, onRejected) {

  // 这里处理一下异常穿透,和then方法只传一个参数或不传参数的情况
  if(typeof onResolved !== 'function'){
    // 这里是箭头函数的简写方式拉,意思是当onResolved不是一个函数的时候,直接把参数返回
    onResolved = value => value;
  }
  if(typeof onRejected !== 'function'){
    onRejected = reason => {
      // 如果onRejected不是一个函数,我们直接将参数,通过throw抛出去就好了
      // 因为我们在下面的公共方法里面已经做了处理,所以直接抛错就可以了
      throw reason;
    }
  }

  // then方法的返回值是一个promise对象,
  // 所以这里要使用new去创建一个promise对象,这样才能支持链式调用
  return new Promise((resolve, reject) => {

    const self = this;

    function commonFn(type){
      try{
        const res = type(self.promiseResult);
        // 判断返回值是不是promise对象
        if(res instanceof Promise){
          // 如果是Promise对象的话,那么它就可以调用then方法,
          // 这个then方法的参数,也就是promise对象的结果,用它来生成外部then方法的返回值
          res.then(v => {
            resolve(v);
          }, r =>{
            reject(r);
          });
        } else {
          // 如果不是promise对象,直接返回成功的promise对象就好了
          resolve(res);
        }
      } catch(err){
        reject(err);
      }
    }

    // 当状态改变成成功的时候执行then方法成功的回调函数onResolved
    if (this.promiseState === FULFILLED) {

      // 这里模仿一下then方法是个微任务,我不会创建微任务,就创建一个宏任务替代一下吧。
      setTimeout(() => {
        // 这里函数更下面的逻辑差不多,就封装了公共方法,看不懂的,可以把这个公共方法注释掉,看下面注释掉的代码
        commonFn(onResolved);
      });
      
      // // 参数是promise对象promiseResult的值
      // const res = onResolved(this.promiseResult);
      // // 判断返回值是不是promise对象
      // if(res instanceof Promise){
      //   // 如果是Promise对象的话,那么它就可以调用then方法,
      //   // 这个then方法的参数,也就是promise对象的结果,用它来生成外部then方法的返回值
      //   res.then(v => {
      //     resolve(v);
      //   }, r =>{
      //     reject(r);
      //   });
      // } else {
      //   // 如果不是promise对象,直接返回成功的promise对象就好了
      //   resolve(res);
      // }
    }

    if(this.promiseState === REJECTED){
      // 这里模仿一下then方法是个微任务,我不会创建微任务,就创建一个宏任务替代一下吧。
      setTimeout(() => {
        // 这里面的方法,和状态为fulfilled差不多,所以封装一个公共方法吧
        commonFn(onRejected);
      });
    }

    // 那如果执行到then方法的时候,promise状态还未改变,我们就需要对其进行缓存,
    // 等promise状态改变的时候,我们再把缓存的函数拿出来执行,
    // 所以缓存,就给promise对象创建一个callbacks的数组进行存储吧
    // 这也是promise可以后指定回调的关键地方
    if(this.promiseState === PENDING){
      // 当状态为pending的时候,缓存回调函数。
      this.callbacks.push({
        onResolved: () => {commonFn(onResolved)},
        onRejected: () => {commonFn(onRejected)}
      })
    }
  });
}


// 写catch方法,上面已经完整的写好了then方法,这里我们就可以直接调用then方法的错误回调就好了
Promise.prototype.catch = function(onRejected){
  // 这里直接将then方法错误回调的返回值返回出去就好了。
  // 因为已经做了错误穿透,所以then方法第一个参数直接传个undefined就行了
  return this.then(undefined, onRejected);
}

// 写私有方法resolve,这里区别原型上的resolve方法哦
Promise.resolve = function(param){
  // 这里应该不用多说了吧,返回的是promise对象,Promise的所有方法都返回的是promise对象
  return new Promise((resolve, reject) => {
    if(param instanceof Promise){
      param.then(v=> {
        resolve(v);
      }, r => {
        reject(r);
      })
    } else {
      resolve(param);
    }
  })
}

// 私有方法reject,返回失败的promise对象
Promise.reject = function(param){
  return new Promise((_, reject) => {
    // 这里就不用判断它是不是promise对象了,直接返回错误的对象就好了
    // 元素的Promise也是这么干的
    reject(param);
  })
}

// 私有方法all
Promise.all = function(promises){
  return new Promise((resolve, reject) => {
    // 定义一个数组缓存成功的值
    let arr = [];
    let count = 0;
    promises.forEach((item, index) => {
      item.then(value => {
        arr[count] = value;
        count++;
        // 当全部成功的时候,给promise对象改变状态和赋值。
        if(count === promises.length){
          resolve(arr);
        }
      }, reason => {
        // 当有一个失败的时候,直接返回错误对象
        reject(reason);
      })
    })
  })
}

// 私有方法race
Promise.race = function(promises){
  return new Promise((resolve, reject) => {
    promises.forEach((item) => {
      // 谁先改变状态,返回值就是谁
      item.then(value => {
        resolve(value);
      }, reason => {
        reject(reason);
      })
    })
  })
}

测试用例

注意:测试用例,一定要在浏览器环境下测试,服务器环境(node)在异步状态下,打印出来的一直都是pending状态,在浏览器上也要等异步时间过了,在去点开查看,要不然,还是pending,目前没搞懂这块打印的机制,有哪位大佬知道的话,可以评论区告诉我一下,让我弥补一下空缺,感激不尽。

测试用例不能一起解开注释去测试,只能一个一个测哈,定义变量太麻烦了,我就没一起编写测试用例了

<!DOCTYPE html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./index.js"></script>
  <title>Promise</title>
</head>
<body>
  <script>

    // 测试1: 同步异步的resolve,resolve方法
    // let p = new Promise((resolve, reject) => {
    //   // resolve('OK');
    //   // reject('error');
    //   // throw 'err';

    //   // 异步
    //   // setTimeout(() => {
    //   //   // resolve('OK');
    //   //   // reject('error');
    //   // }, 200)
    // });
    // console.log(p);

    // 测试2:传入的值为promise对象时,输出的值
    // let p = Promise.resolve( new Promise((resolve, reject) => {
    //   resolve('333');
    // }));
    // let p = Promise.reject( new Promise((resolve, reject) => {
    //   reject('333');
    // }));

    // 测试3:测试then方法的返回值, 为正常js类型和为promise对象
    // let p = Promise.resolve('123');
    // let p = Promise.reject('123');
    // const res = p.then((value) => {
    //   console.log(value);
    //   // return 'ok';
    //   // return new Promise((resolve, reject) => {
    //   //   // resolve('成功啦');
    //   //   // reject('失败啦');
    //   // })
    // }, (reason) => {
    //   return new Promise((resolve, reject) => {
    //     // resolve('成功啦');
    //     // reject('失败啦');
    //     // throw 'oh on';
    //   });
    // })
    // console.log(res);


    // 测试4:给promise指定多个回调,和异常穿透
    // let p = Promise.resolve('123');
    // let p = Promise.reject('123');
    // p.then((value) => {
    //   console.log(111, value);
    //   return '成功啦'
    // }, (reason) => {
    //   console.log(222, reason);
    //   return '失败啦'
    // });
    // p.then((value) => {
    //   console.log(222, value);
    // });

    // p.catch((reason) => {
    //   console.log("555", reason);
    // });




    // 测试5: 测试all方法和race方法
    // const p1 = new Promise((resolve, reject) => {
    //   setTimeout(() => {
    //     resolve('111');
    //   })
    // })
    // const p2 = new Promise((resolve, reject) => {
    //   setTimeout(() => {
    //     resolve('222');
    //     // reject('222');
    //   })
    // })
    // const p3 =  Promise.resolve('3333');
    // // const res = Promise.race([p1, p2, p3]);
    // const res = Promise.all([p1, p2, p3]);
    // console.log(res);
  </script>
</body>
</html>

写在最后

没想到这次失算了,代码加测试用例加注释,竟然画了2个多小时,哈哈哈,平常写Promise只要20-30分钟,没想到写注释这么画时间,哦,这后面还包括了测试的时间,测试出来了几个bug,已经修复了,哈哈哈。不会有人测试用例打印出的结果看不懂吧?如果看不懂说明知识点没掌握透哦。也可能是我写了bug,哈哈哈,有问题大家评论区说吧。

码了五千字,花了一天时间,希望能给不了解promise的小伙伴,带来一些理解吧。技术大佬就随便看看吧。嘿嘿。能给点指导就更好了。

分享一件高兴的事

找了之前带我的同事要到了react源码的教学视频,白嫖的,爽歪歪,这几个月每天晚上又有事干了。 希望我学完以后,能搞个react源码的专题,哈哈哈。