promise实现及详细注释(符合A+规范)

134 阅读8分钟

promise 简介

promise 是异步编程的一种解决方案,解决了回调地狱的问题,现在已经是 JavaScriptDOM 提供异步返回值的正式方法。所有未来的异步 DOM API 都会使用它们。

promise 与回调地狱

回调地狱会造成严重的信任问题,比如有时候调用 ajax(..),属于某个第三方提供的工具,不是你编写的代码,也不在你的直接控制下。这称为控制反转,也就是把自己程序一部分的执行控制交给某个第三方。而promise实现的是控制反转的反转,它实现的是:希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,可能上一步成功,也有可能上一步失败了。

建议:关于异步与promise的详细内容大家可以翻阅一下**《你不知道的JS(中卷)》**

promise 实现

实现参考

通过阅读A+规范,里面有更加具体的promise定义,如下

  1. “promise” is an object or function with a then method whose behavior conforms to this specification.
  2. “thenable” is an object or function that defines a then method.
  3. “value” is any legal JavaScript value (including undefined, a thenable, or a promise).
  4. “exception” is a value that is thrown using the throw statement.
  5. “reason” is a value that indicates why a promise was rejected.

字都认识的情况下还是翻译一下吧。

  1. promise 具有一个then方法
  2. promise有一个value值,代表决议为完成的值
  3. promise有一个reason值,给出了promise决议结果为rejected的理由

继续阅读规范中的requirements,可以发现实现promise就相当于实现每条规则,真正可扩展(大显身手)的空间并不多。下面就让我们按照A+规范一步步实现吧!

具体实现

part 1

promise定义的第一条就给出了promise是一个对象或函数,这里我们使用class类来实现,本质上都是一样的。在类的构造函数中需要传递一个执行器,它将立即执行。这个执行器需要传入两个参数(决议函数):

  1. resolve :异步操作执行成功后的回调函数,标识完成;
  2. reject:异步操作执行失败后的回调函数,标识拒绝
class MyPromise{
  constructor(excutor){
    excutor(this.resolve(), this.reject())
  }
  resolve = () => {
  }
  reject = () => { 
  }
}
module.exports = MyPromise

有了决议函数,我们该思考,决议函数具体该怎么做来标识完成或拒绝。简单来说,决议函数所做的就是更改状态。在规范中对promise的状态做出了非常详细的规定。

  1. When pending, a promise:

    1.1. may transition to either the fulfilled or rejected state.

  2. When fulfilled, a promise:

    2.1. must not transition to any other state.

    2.2. must have a value, which must not change.

  3. When rejected, a promise: 3.1. must not transition to any other state.

    3.2 must have a reason, which must not change.

ok,继续翻译一下😁

  1. 这里有三种状态,包括:
  • 成功 fulfilled
  • 失败 rejected
  • 等待 pending
  1. 状态之间的转化
  • pending->fulfilled
  • pending->rejected
const PENDING = 'pending' 
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
  constructor(excutor){
    excutor(this.resolve(), this.reject())
  }
  status = PENDING
  resolve = () =>{
        //如果状态不是等待,return,因为只有pending状态能转化为其他两种状态
        if (this.status != PENDING) return
        // 更改状态为成功
        this.status = FULFILLED
  }
  reject = () =>{
      if (this.status != PENDING) return
      // 更改状态为失败
      this.status = REJECTED
  }
}

then方法

A promise must provide a then method to access its current or eventual value or reason.

A promise’s then method accepts two arguments:

promise.then(onFulfilled, onRejected)

onFulfilled标识状态决议为fulfilled后的回调,onRejected表示状态决议为rejected后的回调。

then (onFulfilled, onRejected){
    if(this.status === FULFILLED){
      onFulfilled()
    }else if(this.status === REJECTED){
      onRejected() 
    }
}

前面规范中提到的valuereason需要对应传递给onFulfilledonRejected,那么value和reason从哪来的,其实就是用户传入的,比如下面传入的就是success。

new Promise((resolve, reject) => {
    resolve('success')
})

所以要在resolvereject中先保存一下valuereason值。

 // 成功之后的值
value = undefined

// 失败之后的原因
reason = undefined
resolve = (value) => {
  if (this.status != PENDING) return
  this.status = FULFILLED
  // 保存成功之后的值
  this.value = value
}
reject = (reason) => {
  if (this.status != PENDING) return
  this.status = REJECTED
  // 保存失败后的原因
  this.reason = reason
}
then(onFulfilled, onRejected){
  if (this.status === FULFILLED) {
    onFulfilled(this.value)
  } else if (this.status === Rejected) {
    onRejected(this.reason)
  }
}

到这里,我们可以测试一下,看看输出什么吧!

let promise = new MyPromise((resolve, reject) => {
    resolve('success')
    reject('fail')

})
promise.then(value =>{
    console.log(value)
},reason => {
    console.log(reason)
})

part 2

前面已经实现出一个promise雏形了,现在then方法中只判断了FULFILLEDRejected两种状态,但当含有异步操作时,这时未决议出结果,状态为PENDING,因此需要在then方法中处理异步的操作。

当判断状态为PENDING时,先将成功和失败回调存储起来,等到有了结果之后再调用,因此定义两个存储回调函数的数组,为什么是数组,这是因为同一个promisethen方法是可以多次调用的,也就是不止传入一个成功或失败回调。

// 成功回调
onFulfilleds = []
// 失败回调
onRejecteds = []
resolve = (value) => {
    ...
  // 异步代码执行完成后判断是否存在成功回调
  while (this.onFulfilleds.length) this.onFulfilleds.shift()(this.value)
}
reject = (reason) => {
    ...
  // 异步代码执行完成后判断是否存在失败回调
  while (this.onRejecteds.length) this.onRejecteds.shift()(this.reason)
}

then(onFulfilled, onRejected){
  if (this.status === FULFILLED) {
    onFulfilled(this.value)
  } else if (this.status === Rejected) {
    onRejected(this.reason)
  } else {
    // 先将回调函数存储起来
    this.onFulfilleds.push(onFulfilled)
    this.onRejecteds.push(onRejected)
  }
}

到这里可以简单的测试一下,看看是不是3s后输出的 success。

let promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('success')
    },3000)
})
promise.then(value =>{
    console.log(value)
},reason => {
    console.log(reason)  
})

promise.then(value =>{
    console.log(value)
},reason => {
    console.log(reason)
})

以上我们多次调用了同一个promisethen方法,得到了期望的结果,但promise并不只是一个单步执行 this-then-that 操作的机制,我们可以把多个 promise 连接到一起以表示一系列异步,实现的核心就是链式流,它依赖于promise的几个固有特性:

• 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。

• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise 就相应地决议。

• 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。

链式流程控制的实现就靠then方法啦,它将this-then-that转化成了this-then-this-then-this...,由promise链式流相关的固有特性得出实现then方法链式调用的几个方面:

  • then方法要返回一个全新的promise对象,能支持链式调用
  • 后面的then方法实际上在为上一个then返回的promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 若回调中返回的是promise,后面then方法的回调也会等待它的决议结果
then(onFulfilled, onRejected){
  //创建一个新的promise对象,支持链式调用
  let promise2 = new MyPromise((resolve, reject) => {
    if (this.status === FULFILLED) {
      // 这里为什么将其变成异步任务呢,因为这时候直接式获取不到promise2的
      setTimeout(() => {
        //拿到当前回调函数的值,用来传递给下一个then
        let x = onFulfilled(this.value)
        // 判断 x 的值是普通值还是promise对象
        // 如果是普通值,直接调用resolve
        // 如果是promise对象查看promise对象返回的结果
        // 根据返回的结果,决定调用resolve还是reject
        resolvePromise(promise2, x, resolve, reject)
      }, 0)
    } else if (this.status === Rejected) {
      setTimeout(() => {
        let x = onRejected(this.reason)
        resolvePromise(promise2, x, resolve, reject)

      }, 0)

    } else {
      // 先将回调函数存储起来
      this.onFulfilleds.push(() => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0);)
      this.onRejecteds.push(() => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      })
    }
  })
  return promise2
}

function resolvePromise(promise2, x, resolve, reject) {
  // 调用自身,造成了promise的循环调用
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))
  }
  if (x instanceof MyPromise) {
    //promise对象
    x.then(resolve, reject)
  } else {
    resolve(x)
  }
}

写到这可以测试一下。

then的参数

想想下面输出什么🤔

let promise = new Promise((resolve,reject)=>{
resolve('success')
})
promise.then().then().then(value=>{console.log(value)})

在实际使用中,then方法的参数是可选的,但我们需要保证链式控制流程正常,也就是能够一直传递下去。因此需要对传参处理一下。

then(onFulfilled, onRejected){
// then中也可以不传递参数,但是要保证能够一直传递下去,直到传递给有参数的
    onFulfilled = onFulfilled ? onFulfilled : value => value
    onRejected = onRejected ? onRejected : reason => { throw reason }
}

不过这样还是有问题的,因为promise中还规定了当then传递非函数值应该替换为相应的默认回调,因此应判断下参数类型是否为函数。

then(onFulfilled, onRejected){
// then中也可以不传递参数,但是要保证能够一直传递下去,直到传递给有参数的
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
}

part 3

为了代码的健壮性,我们需要添加一些错误捕捉try-catch,在失败回调中能够捕获到。

  1. 捕获执行器的异常
try {
  executor(this.resolve, this.reject)
} catch (e) {
  this.reject(e)
}
  1. 捕获then回调函数中发生的错误,在下一个then的失败回调函数中捕获到
setTimeout(() => {
  try {
    let x = successCallback(this.value)
    resolvePromise(promise2, x, resolve, reject)
  } catch (e) {
    // 把这个错误传给下一个promise的回调函数
    reject(e)
  }
}, 0);

到这里,一个简单的promise就实现完了。至于其他API,包括Promise.all, Promise.race等等,大家可以自己实现一下,完整代码中也有。

总结

  1. 在实现之前,需要先掌握promise怎么用,如果连使用都不清楚的话,实现也没有意义
  2. 按照A+规范一步步实现,每实现一步测试一下,这可不是一蹴而就的过程
  3. 实现完成后可以用promises-aplus-tests去测试实现的promise是否完全符合A+规范
  4. 完整代码请戳