手写一个Promise/A+,完美通过官方872个测试用例

前段时间我用两篇文章深入讲解了异步的概念和Event Loop的底层原理,然后还讲了一种自己实现异步的发布订阅模式:

setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop

从发布订阅模式入手读懂Node.js的EventEmitter源码

本文会讲解另一种更现代的异步实现方案:Promise。Promise几乎是面试必考点,所以我们不能仅仅会用,还得知道他的底层原理,学习他原理的最好方法就是自己也实现一个Promise。所以本文会自己实现一个遵循Promise/A+规范的Promise。实现之后,我们还要用Promise/A+官方的测试工具来测试下我们的实现是否正确,这个工具总共有872个测试用例,全部通过才算是符合Promise/A+规范,下面是他们的链接:

Promise/A+规范: https://github.com/promises-aplus/promises-spec

Promise/A+测试工具: https://github.com/promises-aplus/promises-tests

本文的完整代码托管在GitHub上: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

Promise用法

Promise的基本用法,网上有很多,我这里简单提一下,我还是用三个相互依赖的网络请求做例子,假如我们有三个网络请求,请求2必须依赖请求1的结果,请求3必须依赖请求2的结果,如果用回调的话会有三层,会陷入“回调地狱”,用Promise就清晰多了:

const request = require("request");

// 我们先用Promise包装下三个网络请求
// 请求成功时resolve这个Promise
const request1 = function() {
   
  const promise = new Promise((resolve) => {
   
    request('https://www.baidu.com', function (error, response) {
   
      if (!error && response.statusCode == 200) {
   
        resolve('request1 success');
      }
    });
  });

  return promise;
}

const request2 = function() {
   
  const promise = new Promise((resolve) => {
   
    request('https://www.baidu.com', function (error, response) {
   
      if (!error && response.statusCode == 200) {
   
        resolve('request2 success');
      }
    });
  });

  return promise;
}

const request3 = function() {
   
  const promise = new Promise((resolve) => {
   
    request('https://www.baidu.com', function (error, response) {
   
      if (!error && response.statusCode == 200) {
   
        resolve('request3 success');
      }
    });
  });

  return promise;
}


// 先发起request1,等他resolve后再发起request2,
// 然后是request3
request1().then((data) => {
   
  console.log(data);
  return request2();
})
.then((data) => {
   
  console.log(data);
  return request3();
})
.then((data) => {
   
  console.log(data);
})

上面的例子里面,then是可以链式调用的,后面的then可以拿到前面resolve出来的数据,我们控制台可以看到三个success依次打出来:

image-20200324164123892

Promises/A+规范

通过上面的例子,其实我们已经知道了一个promise长什么样子,Promises/A+规范其实就是对这个长相进一步进行了规范。下面我会对这个规范进行一些讲解。

术语

  1. promise:是一个拥有 then 方法的对象或函数,其行为符合本规范

  2. thenable:是一个定义了 then 方法的对象或函数。这个主要是用来兼容一些老的Promise实现,只要一个Promise实现是thenable,也就是拥有then方法的,就可以跟Promises/A+兼容。

  3. value:指reslove出来的值,可以是任何合法的JS值(包括 undefined , thenable 和 promise等)

  4. exception:异常,在Promise里面用throw抛出来的值

  5. reason:拒绝原因,是reject里面传的参数,表示reject的原因

Promise状态

Promise总共有三个状态:

  1. pending: 一个promise在resolve或者reject前就处于这个状态。
  2. fulfilled: 一个promise被resolve后就处于fulfilled状态,这个状态不能再改变,而且必须拥有一个不可变的值(value)。
  3. rejected: 一个promise被reject后就处于rejected状态,这个状态也不能再改变,而且必须拥有一个不可变的拒绝原因(reason)。

注意这里的不可变指的是===,也就是说,如果value或者reason是对象,只要保证引用不变就行,规范没有强制要求里面的属性也不变。Promise状态其实很简单,画张图就是:

image-20200324173555225

then方法

一个promise必须拥有一个then方法来访问他的值或者拒绝原因。then方法有两个参数:

promise.then(onFulfilled, onRejected)
参数可选

onFulfilledonRejected 都是可选参数。

  • 如果 onFulfilled 不是函数,其必须被忽略
  • 如果 onRejected 不是函数,其必须被忽略
onFulfilled 特性

如果 onFulfilled 是函数:

  • promise 执行结束后其必须被调用,其第一个参数为 promise 的终值value
  • promise 执行结束前其不可被调用
  • 其调用次数不可超过一次
onRejected 特性

如果 onRejected 是函数:

  • promise 被拒绝执行后其必须被调用,其第一个参数为 promise 的据因reason
  • promise 被拒绝执行前其不可被调用
  • 其调用次数不可超过一次
多次调用

then 方法可以被同一个 promise 调用多次

  • promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
  • promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
返回

then 方法必须返回一个 promise 对象。

promise2 = promise1.then(onFulfilled, onRejected); 
  • 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行 Promise 解决过程[[Resolve]](promise2, x)
  • 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
  • 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值
  • 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的据因

规范里面还有很大一部分是讲解Promise 解决过程的,光看规范,很空洞,前面这些规范已经可以指导我们开始写一个自己的Promise了,Promise 解决过程会在我们后面写到了再详细讲解。

自己写一个Promise

我们自己要写一个Promise,肯定需要知道有哪些工作需要做,我们先从Promise的使用来窥探下需要做啥:

  1. 新建Promise需要使用new关键字,那他肯定是作为面向对象的方式调用的,Promise是一个类。关于JS的面向对象更详细的解释可以看这篇文章。
  2. 我们new Promise(fn)的时候需要传一个函数进去,说明Promise的参数是一个函数
  3. 构造函数传进去的fn会收到resolvereject两个函数,用来表示Promise成功和失败,说明构造函数里面还需要resolvereject这两个函数,这两个函数的作用是改变Promise的状态。
  4. 根据规范,promise有pendingfulfilledrejected三个状态,初始状态为pending,调用resolve会将其改为fulfilled,调用reject会改为rejected
  5. promise实例对象建好后可以调用then方法,而且是可以链式调用then方法,说明then是一个实例方法。链式调用的实现这篇有详细解释,我这里不再赘述。简单的说就是then方法也必须返回一个带then方法的对象,可以是this或者新的promise实例。

构造函数

为了更好的兼容性,本文就不用ES6了。

// 先定义三个常量表示状态
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

function MyPromise(fn) {
   
  this.status = PENDING;    // 初始状态为pending
  this.value = null;        // 初始化value
  this.reason = null;       // 初始化reason
}

resolvereject方法

根据规范,resolve方法是将状态改为fulfilled,reject是将状态改为rejected。

// 这两个方法直接写在构造函数里面
function MyPromise(fn) {
   
  // ...省略前面代码...
  
  // 存一下this,以便resolve和reject里面访问
  var that = this;
  // resolve方法参数是value
  function resolve(value) {
   
    if(that.status === PENDING) {
   
      that.status = FULFILLED;
      that.value = value;
    }
  }
  
  // reject方法参数是reason
  function reject(reason) {
   
    if(that.status === PENDING) {
   
      that.status = REJECTED;
      that.reason = reason;
    }
  }
}

调用构造函数参数

最后将resolvereject作为参数调用传进来的参数,记得加上try,如果捕获到错误就reject

function MyPromise(fn) {
   
  // ...省略前面代码...
  
  try {
   
    fn(resolve, reject);
  } catch (error) {
   
    reject(error);
  }
}

then方法

根据我们前面的分析,then方法可以链式调用,所以他是实例方法,而且规范中的API是promise.then(onFulfilled, onRejected),我们先把架子搭出来:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
   }

then方法里面应该干什么呢,其实规范也告诉我们了,先检查onFulfilledonRejected是不是函数,如果不是函数就忽略他们,所谓“忽略”并不是什么都不干,对于onFulfilled来说“忽略”就是将value原封不动的返回,对于onRejected来说就是返回reasononRejected因为是错误分支,我们返回reason应该throw一个Error:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
   
  // 如果onFulfilled不是函数,给一个默认函数,返回value
  var realOnFulfilled = onFulfilled;
  if(typeof realOnFulfilled !== 'function') {
   
    realOnFulfilled = function (value) {
   
      return value;
    }
  }

  // 如果onRejected不是函数,给一个默认函数,返回reason的Error
  var realOnRejected = onRejected;
  if(typeof realOnRejected !== 'function') {
   
    realOnRejected = function (reason) {
   
      if(reason instanceof Error) {
   
        throw reason;
      } else {
   
        throw new Error(reason)
      }
    }
  }
}

参数检查完后就该干点真正的事情了,想想我们使用Promise的时候,如果promise操作成功了就会调用then里面的onFulfilled,如果他失败了,就会调用onRejected。对应我们的代码就应该检查下promise的status,如果是FULFILLED,就调用onFulfilled,如果是REJECTED,就调用onRejected:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
   
  // ...省略前面代码...

  if(this.status === FULFILLED) {
   
    onFulfilled(this.value)
  }

  if(this.status === REJECTED) {
   
    onRejected(this.reason);
  }
}

再想一下,我们新建一个promise的时候可能是直接这样用的:

new Promise(fn).then(onFulfilled, onRejected);

上面代码then是在实例对象一创建好就调用了,这时候fn里面的异步操作可能还没结束呢,也就是说他的status还是PENDING,这怎么办呢,这时候我们肯定不能立即调onFulfilled或者onRejected的,因为fn到底成功还是失败还不知道呢。那什么时候知道fn成功还是失败呢?答案是fn里面主动调resolve或者reject的时候。所以如果这时候status状态还是PENDING,我们应该将onFulfilledonRejected两个回调存起来,等到fn有了结论,resolve或者reject的时候再来调用对应的代码。因为后面then还有链式调用,会有多个onFulfilledonRejected,我这里用两个数组将他们存起来,等resolve或者reject的时候将数组里面的全部方法拿出来执行一遍

// 构造函数
function MyPromise(fn) {
   
  // ...省略其他代码...
  
  // 构造函数里面添加两个数组存储成功和失败的回调
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];
  
  function resolve(value) {
   
    if(that.status === PENDING) {
   
      // ...省略其他代码...
      // resolve里面将所有成功的回调拿出来执行
      that.onFulfilledCallbacks.forEach(callback => {
   
        callback(that.value);
      })</
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值