【手撕系列】手撕Promise -- 一文带你根据Promises/A+规范完美实现Promise!

549 阅读26分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

本文根据Promise A+规范,带你一步一步由浅入深实现Promise,耐心看完绝对能够理解并实现!

源码在我的github中,有需要可以自取:github.com/Plasticine-…

1. 基本功能实现

首先要明确Promise是一个构造函数,能够接收一个executor参数,该参数是一个函数,它接收两个参数executor(resolve, reject)

那么在我们的MyPromise的构造函数中是不是就应该要去执行这个executor函数,并且传入resolvereject参数,这两个参数是函数,并且在我们的MyPromise中去实现

由于每一个Promise实例的resolvereject函数都应当是相互独立的,也就是都是属于实例自身的,因此不适合定义在构造函数的prototype上,在ES6 class的角度来看,就是说不应当将resolvereject函数定义为类的方法,而应当在构造函数内部定义,这样就能保证它们是会在每个实例实例化调用构造函数的时候在内存中创建多个,如果定义为方法的话,就会挂到构造函数的prototype

根据以上的特点,我们可以先写一个整体的框架

class MyPromise {
  constructor(executor) {
    const resolve = () => {}
    const reject = () => {}
    
    executor(resolve, reject)
  }
}

我们都知道,Promise是有状态的,根据Promises A+规范,有三种状态

A promise must be in one of three states: pending, fulfilled, or rejected.

那么现在我们就给我们的MyPromise定义三个状态常量

/**
 * @description MyPromise 的状态
 */
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

每个Promise实例在初始的时候都是pending状态,除非外部调用了resolve或者reject函数才会改变promise实例的状态,因此构造函数中应当先初始化状态为pending

constructor(executor) {
  this.status = PENDING
}

其次,根据Promises A+规范,每个Promise实例都能够调用then方法,由onFulfilled回调去处理resolved状态的value,以及由onRejected回调去处理rejected状态的reason,那么我们还需要给实例维护一个valuereason以便之后能够被相应的回调使用

constructor(executor) {
  this.value = undefined
  this.reason = undefined
}

这里初始化为undefined是因为外部如果调用resolve或者reject函数时没有传入参数的话,在js中未传入实参时去访问形参就是undefined,因此onFulfilled/onRejected回调中的value/reason也应当是undefined,这是为了和js的函数特性统一

function foo(text) {
  console.log(text)
}

foo() // => undefined

接下来就是在resolve/reject中处理状态的变更以及设置相应的value/reason

constructor(executor) {
  const resolve = (value) => {
    if (this.status === PENDING) {
      this.status = FULFILLED
      this.value = value
    }
  }
  
  const reject = (reason) => {
    if (this.status === PENDING) {
      this.status = REJECTED
      this.reason = reason
    }
  }
  
  executor(resolve, reject)
}

这里有几点值得注意:

  1. 状态的变更只能是从pending变到fulfilled/rejected,而不能是其他的变化,因此在变更状态之前一定要判断一下当前状态是不是pending
  2. value/reason的修改一定是在状态变更后才生效的,因此也要放到判断语句中
  3. 最重要的一点!!!resolve/reject函数只能用箭头函数去定义,一定不能用普通函数去定义,这涉及到箭头函数和普通函数中this指向的问题
    1. 因为需要在resolve/reject中使用到this,并且一定要保证this是指向MyPromise实例的
    2. 普通函数的 this 指向是在外部调用的时候决定的,会因为this的默认绑定、隐式绑定、显式绑定等特性导致this的指向不明确,不能保证指向实例本身
    3. 箭头函数中没有this,根据js词法作用域的特点,会使用函数定义时的父函数定义域中的this,也就是constructor函数中的this
  4. 而箭头函数要用 const 关键字声明一个引用变量去指向它,因此要在执行 executor 之前定义,否则会由于暂时性死区导致无法找到 resolve, reject 函数

接下来就是then方法的实现了,then方法接收两个参数,onFulfilledonRejected,是两个回调函数,根据Promises A+规范,调用then方法会在fulfilled状态时执行onFullfilled回调,在rejected状态时执行onRejected回调,根据这一特性,可以写出如下代码:

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

现在就已经得到一个可以实现基本功能的Promise了,目前的完整代码如下:

/**
 * @description MyPromise 的状态
 */
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

class MyPromise {
  constructor(executor) {
    this.status = PENDING // 初始状态为 PENDING
    this.value = undefined // resolve 出去的值
    this.reason = undefined // reject 出去的原因

    const resolve = (value) => {
      // 只有在 PENDING 状态时才能转换到 FULFILLED 状态
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value
      }
    }

    const reject = (reason) => {
      // 只有在 PENDING 状态时才能转换到 REJECTED 状态
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason
      }
    }

    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    // 根据状态去判断执行哪一个回调
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }

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

测试一下

function foo() {
  return new MyPromise((resolve, reject) => {
    resolve('resolved value')
  })
}

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

// => resolved value
function foo() {
  return new MyPromise((resolve, reject) => {
    reject('rejected reason')
  })
}

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

// => rejected reason

2. 异常处理

目前我们的MyPromise在遇到异常的时候会怎样呢?能够像Promise那样被onRejected捕获到吗?

很明显是不行的,因为我们并没有对异常进行任何的处理。那么思考一下异常的处理应该加在什么地方才能让异常被捕获到并且传给onRejected回调去处理呢?

首先要明确异常从哪里来,异常肯定是从外部调用构造函数时的executor函数中来的,因为主要的业务逻辑都是在executor中编写的

因此如果在调用executor的过程中出现了异常,我们应当在我们的MyPromise中去捕获它,并且因为希望被onRejected处理,因此需要将状态改为rejected,并将reason设置为捕获到的异常,这部分逻辑不就是reject函数做的事情吗?因此我们可以直接复用,调用reject即可

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

现在异常就能够被onRejected处理了

function foo() {
  return new MyPromise((resolve, reject) => {
    throw new Error('something wrong...')
  })
}

foo().then(
  (value) => {
    console.log(`resolved: ${value}`)
  },
  (reason) => {
    console.log(`rejected: ${reason}`)
  }
)

// => rejected: Error: something wrong...

3. 使用发布-订阅模式解决异步和多次调用的问题

目前我们的MyPromise中如果遇到了异步执行的情况会怎么样?比如说在executor中,延时两秒才去执行resolve函数,那么能被then方法的onFulfilled处理吗?

function foo() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('resolved value'), 2000)
  })
}

foo().then(
  (value) => {
    console.log(`resolved: ${value}`)
  },
  (reason) => {
    console.log(`rejected: ${reason}`)
  }
)

// =>

可以看到什么都没有输出,因为executor的执行是同步的,遇到setTimeout的时候会将执行回调放到事件循环的宏任务队列中,接着立刻去执行then方法了, 而此时由于还没有调用resolve,因此状态还是pending,那么then自然也就无法处理了

等到两秒后定时器中的回调执行了,改变了状态,但此时js线程中的代码早已经执行完了,也就是then早就结束了,因此没有别的代码能够处理resolve后的resolved状态

或许我们会想出下面这样的解决方案

function foo() {
  return new MyPromise((resolve, reject) => {
    // resolve('resolved value')
    // reject('rejected reason')
    // throw new Error('something wrong...')

    setTimeout(() => resolve('resolved value'), 2000)
  })
}

const myPromise = foo()

// 2.5 秒后再去执行 then 方法处理 resolved 状态
setTimeout(() => {
  myPromise.then(
    (value) => {
      console.log(`resolved: ${value}`)
    },
    (reason) => {
      console.log(`rejected: ${reason}`)
    }
  )
}, 2500)

// => resolved: resolved value

注意:两个定时器几乎是同时开始计时的,因此最终打印出**resolved: resolved value**所花费的时间不是**2 + 2.5 = 4.5秒**,而是**2.5秒**

这种方案貌似确实能够解决异步调用的问题,但它真的解决了吗?

试想一下,如果我们现在不是用定时器,而是一个axios/fetchajax请求呢?我们只会在请求有结果之后才去调用resolve函数,还能这样定死一个2.5秒的定时器去调用then方法吗?

显然是不行的,因为并不知道这个ajax请求具体会花费多少时间,如果运气好,它在2.5秒内返回结果了,那么then还是能够正常处理结果的,但是如果超出了2.5秒就不行了,难道要设置一个超级长时间的定时器,到期后再去执行then方法吗?显然太离谱了

这就要考虑再改进一下我们的MyPromise了,我们需要用到**发布-订阅**这一设计模式来解决这一问题

首先思考一下,之所以会出现异步调用无法触发onResolved回调,不正是因为在同步代码中执行then方法的时候,异步代码还没调用resolve呢,因此整个MyPromise实例的状态还是pending状态

既然如此,那么我们可以在then方法中处理pending状态呀,维护一个容器,专门用于存放onResolved回调(因为可能会调用多次then方法,每个then方法都有自己的onResolved回调),然后在异步代码调用resolve的时候,从这个容器中依次取出onResolved回调并且执行不就可以了吗!

事实上:

  • then中处理pending,将onResolvedonRejected分别加入到对应容器中这一过程就是“订阅”的过程,订阅fulfilledrejected事件
  • resolve将容器中的onResolved回调依次取出执行的过程就是“发布”的过程,发布fulfilled消息
  • reject也是类似的,只是它是从存放onRejected回调的容器中去取回调而已,也是一个“发布”的过程,发布的是rejected消息

直接看代码吧!

then(onFulfilled, onRejected) {
  // 根据状态去判断执行哪一个回调
  if (this.status === FULFILLED) {
    onFulfilled(this.value)
  }

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

  if (this.status === PENDING) {
    // pending 状态下需要 “订阅” fulfilled 和 rejected 事件
    this.onFulfilledCallbackList.push(() => onFulfilled(this.value))
    this.onRejectedCallbackList.push(() => onRejected(this.reason))
  }
}

const resolve = (value) => {
  // 只有在 PENDING 状态时才能转换到 FULFILLED 状态
  if (this.status === PENDING) {
    this.status = FULFILLED
    this.value = value

    // 触发 resolve 行为,开始 “发布” resolved 事件
    this.onFulfilledCallbackList.forEach((fn) => fn())
  }
}

const reject = (reason) => {
  // 只有在 PENDING 状态时才能转换到 REJECTED 状态
  if (this.status === PENDING) {
    this.status = REJECTED
    this.reason = reason

    // 触发 reject 行为,开始 “发布” rejected 事件
    this.onRejectedCallbackList.forEach((fn) => fn())
  }
}

现在就可以正常处理异步代码了

function foo() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('resolved value'), 2000)
  })
}

foo().then(
  (value) => {
    console.log(`resolved: ${value}`)
  },
  (reason) => {
    console.log(`rejected: ${reason}`)
  }
)

// => resolved: resolved value

并且这样设计还有一个好处,对于多次调用了then方法的时候,我们肯定是希望按照调用then方法的顺序去执行它们当中的回调的,而由于我们维护的两个容器实际上就是一个队列,回调都是先进先出的,这样就保证了回调执行的顺序和then方法的调用顺序是一致的了

function foo() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('resolved value'), 2000)
  })
}

const myPromise = foo()

myPromise.then(
  (value) => {
    console.log(`resolved1: ${value}`)
  },
  (reason) => {
    console.log(`rejected1: ${reason}`)
  }
)

myPromise.then(
  (value) => {
    console.log(`resolved2: ${value}`)
  },
  (reason) => {
    console.log(`rejected2: ${reason}`)
  }
)

myPromise.then(
  (value) => {
    console.log(`resolved3: ${value}`)
  },
  (reason) => {
    console.log(`rejected3: ${reason}`)
  }
)

/**
 => resolved1: resolved value
    resolved2: resolved value
    resolved3: resolved value
 */

4. 解决链式调用的问题

原生的Promise是支持链式调用的

const promise = new Promise((resolve, reject) => {
  resolve('plasticine')
})
promise
  .then((value) => {
    console.log(`then1 -- ${value}`)
    return value
  })
  .then((value) => {
    console.log(`then2 -- ${value}`)
    return new Promise((resolve, reject) => resolve(value))
  })
  .then((value) => {
    console.log(`then3 -- ${value}`)
    return Promise.resolve(value)
  })
  .then((value) => {
    console.log(`then4 -- ${value}`)
  })

/**
=>  then1 -- plasticine
    then2 -- plasticine
    then3 -- plasticine
    then4 -- plasticine
 */

目前我们的MyPromise还不支持这样的功能,为什么能够链式调用呢?那肯定是因为then方法返回的是Promise实例呗,事实上Promises A+规范中明确规定了:

then must return a promise. promise2 = promise1.then(onFulfilled, onRejected);

  1. If either onFulfilledor onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
  2. If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
  3. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
  4. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

4.1 then中返回Promise实现链式调用

那么根据这个规范,我们可以修改一下then方法的实现,包裹一层MyPromise对象promise2,并且将这个对象返回

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    // 根据状态去判断执行哪一个回调
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }

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

    if (this.status === PENDING) {
      // pending 状态下需要 “订阅” fulfilled 和 rejected 事件
      this.onFulfilledCallbackList.push(() => onFulfilled(this.value))
      this.onRejectedCallbackList.push(() => onRejected(this.reason))
    }
  })

  return promise2
}

4.2 捕获then中两个回调的返回值x

规范的第一条里还提到了,onFulFilledonRejected回调会返回一个x,那么就加上呗

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    // 根据状态去判断执行哪一个回调
    if (this.status === FULFILLED) {
      let x = onFulfilled(this.value)
    }

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

    if (this.status === PENDING) {
      // pending 状态下需要 “订阅” fulfilled 和 rejected 事件
      this.onFulfilledCallbackList.push(() => {
        let x = onFulfilled(this.value)
      })
      this.onRejectedCallbackList.push(() => {
        let x = onRejected(this.reason)
      })
    }
  })

  return promise2
}

然后这个x是会被resolve出去的,这样才能够被下一个then方法接收到

那么就要考虑一下x会是什么了,从TS的角度来说,xany类型的,可能是基本数据类型也可能是引用数据类型,这都无所谓,麻烦的是x还可能是MyPromise对象,那么这就需要好好研究一下了

因为如果直接把MyPromise对象给resolve出去,下一个then拿到的是一个MyPromise对象,它没办法拿到真正想要的value,因此对于xMyPromise对象的情况,应当执行它里面的resolve函数


4.3 处理then回调执行过程中抛出的异常

规范的第二条中提到了对于onFulfilledonRejected中的异常,会作为promise2reasonreject出去,那我们就try/catch捕获一下,遇到异常就调用promise2reject即可

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    // 根据状态去判断执行哪一个回调
    if (this.status === FULFILLED) {
      try {
        let x = onFulfilled(this.value)
      } catch (e) {
        reject(e)
      }
    }

    if (this.status === REJECTED) {
      try {
        let x = onRejected(this.reason)
      } catch (e) {
        reject(e)
      }
    }

    if (this.status === PENDING) {
      // pending 状态下需要 “订阅” fulfilled 和 rejected 事件
      this.onFulfilledCallbackList.push(() => {
        try {
          let x = onFulfilled(this.value)
        } catch (e) {
          reject(e)
        }
      })
      this.onRejectedCallbackList.push(() => {
        try {
          let x = onRejected(this.reason)
        } catch (e) {
          reject(e)
        }
      })
    }
  })

  return promise2
}

4.4 resolvePromise处理x

接下来我们就应该去处理一下x了,规范第一条中说到[[Resolve]](promise2, x)其实就是要调用[[Resolve]]函数去处理promise2实例以及返回的x

同样的,规范中有说明,这个[[Resolve]]函数在规范中的命名为resolvePromise,那我们就应当在拿到promise2x之后去调用这样一个函数

const promise2 = new MyPromise((resolve, reject) => {
  // 根据状态去判断执行哪一个回调
  if (this.status === FULFILLED) {
    try {
      let x = onFulfilled(this.value)
      resolvePromise(promise2, x)
    } catch (e) {
      reject(e)
    }
  }
  ...
}

4.4.1 异步执行resolvePromise保证参数能够获取到

但是有一个问题,promise2能够拿得到吗?promise2是在MyPromise的构造函数执行完毕后才会得到的,而现在我们却在构造函数内部提前要用到promise2实例,这显然有点问题

其次,我们之后是需要调用promise2resolve/reject来处理x的,但是它们是定义在MyPromise构造函数内部的,因此resolvePromise中无法访问到,因此我们还需要将resolve/reject也传给resolvePromise函数

const promise2 = new MyPromise((resolve, reject) => {
  // 根据状态去判断执行哪一个回调
  if (this.status === FULFILLED) {
    try {
      let x = onFulfilled(this.value)
      resolvePromise(promise2, x, resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  ...
}

好了,那么接下来就要解决如何获取到promise2的问题了,这个问题在Promises A+规范中也有提到

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

可以看到,可以使用宏任务或者微任务的方式去实现,这样一来,整个构造函数的执行会在js线程执行的时候去执行,宏任务会被添加到宏任务队列中,然后到了执行宏任务的时候,promise2实例已经是创建好了的,因此resolvePromise就可以正常使用它了

当然,也可以用微任务的方式,原生的Promise就是以微任务的方式实现的,这里为了简单起见,直接用setTimeout实现,它是宏任务的方式实现的

then(onFulfilled, onRejected) {
  const promise2 = new MyPromise((resolve, reject) => {
    // 根据状态去判断执行哪一个回调
    if (this.status === FULFILLED) {
      // 以宏任务的方式执行 resolvePromise 才能保证拿到 promise2 实例
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value)
          resolvePromise(promise2, x)
        } catch (e) {
          reject(e)
        }
      }, 0)
    }
  })
}

这里**setTimeout**的延时默认就是0,不填也可以,我填了只是为了强调一下这里使用**setTimeout**仅仅是为了以宏任务的方式去执行**resolvePromise**,让他在事件循环的下一轮执行,而不是立即执行,且要注意的是,虽然是**0ms**的延迟,但实际上还是会有至少**4ms**的延迟,这一点**MDN**上有解释,自行查阅即可


4.4.2 实现resolvePromise

接下来就要实现一下resolvePromise了,resolvePromise的实现也需要按照Promises A+的规范去实现 image.png


4.4.2.1 循环引用时抛出异常

首先根据2.3.1,如果promisex引用的是同一个对象,则应该reject一个TypeError作为reason

先用原生Promise示范一下,加深理解

const promise1 = new Promise((resolve, reject) => {
  resolve('plasticine')
})

const promise2 = promise1.then(
  (value) => {
    return promise2
  },
  (reason) => {
    return reason
  }
)

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

// => TypeError: Chaining cycle detected for promise #<Promise>

那么我们可以在resolvePromise中判断一下,promise === xreject一个TypeError出去

/**
 *
 * @param {MyPromise} promise MyPromise 实例
 * @param {any} x 前一个 MyPromise resolve 出来的值
 * @param {Function} resolve promise 对象构造函数中的 resolve
 * @param {Function} reject promise 对象构造函数中的 reject
 */
function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }
}

4.4.2.2 判断x是否是MyPromise实例

接下来看一下2.3.32.3.2可以跳过,实际上就是告诉我们要保持promise的状态而已)

2.3.3需要判断一下x是否是一个object/function

然后2.3.3.1又要令then = x.then,然后会判断then是否是一个函数

这样做的目的就是用于判断x是否是一个MyPromise对象,有then方法则将其视为是MyPromise对象

**then = x.then**这一过程需要注意到一点,**x.then**可能被**Object.defineProperty**设置了**getter**劫持,如果**getter**中抛出了异常,需要将这个异常作为**reason**把它**reject**出去,这也正是**2.3.3.2**中规定的

Object.defineProperty(x, 'then', {
  get() {
    throw new Error('something wrong...')
  }
})

因此我们需要用try/catch包裹一下let then = x.then,然后根据2.3.3.3.12.3.3.3.2,当x是一个MyPromise对象的时候,我们需要调用它的then方法,并且需要显式绑定thisx,同时传入两个回调:resolvePromiserejectPromise

**resolvePromise**不是外面的整个**resolvePromise(promise, x, resolve, reject)**,而是另外定义的,这里我们直接用箭头函数的形式传入,是两个匿名函数

且根据2.3.3.3.1,resolvePromise回调接收一个参数y,然后会递归调用resolvePromise(promise, y, resolve, reject),因为如果x是一个MyPromise的话,它的then当中仍然可能返回MyPromise对象,因此需要递归处理

根据以上几点,我们现在得到的resolvePromise(promise, x, resolve, reject)如下:

/**
 * 根据 x 的类型作出不同的处理
 * x 为非 MyPromise 对象的时候 -- 直接 resolve
 * x 为 MyPromise 对象的时候 -- 调用其 resolve
 *
 * 剩下的还有一些细节会具体在代码中注释
 *
 * @param {MyPromise} promise MyPromise 实例
 * @param {any} x 前一个 MyPromise resolve 出来的值
 * @param {Function} resolve promise 对象构造函数中的 resolve
 * @param {Function} reject promise 对象构造函数中的 reject
 */
function resolvePromise(promise, x, resolve, reject) {
  // 根据 Promises A+ 规范 2.3.1: promise 和 x 指向同一个引用时应当 reject 一个 TypeError
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }

  // 2.3.3: x 是 object or function 时的情况
  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // typeof null === 'object',因此需要额外判断一下排除 x 是 null 的情况

    try {
      // x.then 可能被 Object.defineProperty 设置了 getter 劫持,并抛出异常,因此要 try/catch 捕获
      let then = x.then

      if (typeof then === 'function') {
        // x 有 then 方法时,就将其视为是 MyPromise 对象
        // 2.3.3.3 执行 x.then,并且要显式绑定 this 指向,且有两个回调 resolvePromise 和 rejectPromise
        then.call(
          x,
          // resolvePromise
          (y) => {
            // 需要递归调用 MyPromise 中 resolve 出去的值,也就是这里的 y
            resolvePromise(promise, y, resolve, reject)
          },
          // rejectPromise
          (r) => {
            // 对于 reject,直接将其 reason 给 reject 出去即可
            reject(r)
          }
        )
      } else {
        // x 没有 then 方法 -- 不需要特殊处理,直接 resolve
        resolve(x)
      }
    } catch (e) {
      reject(e)
    }
  } else {
    // 基本数据类型 -- 直接 resolve
    resolve(x)
  }
}

4.4.2.3 防止多次执行then中的回调

根据2.3.3.3.3,当两个回调都被执行的时候,我们应当只执行第一个,什么意思呢?用原生Promise演示一下

const promise1 = new Promise((resolve, reject) => {
  resolve('plasticine')
})

const promise2 = promise1.then(
  (value) => {
    return new Promise((resolve, reject) => {
      resolve(value)
      reject(new Error('something wrong...'))
    })
  },
  (reason) => {
    return reason
  }
)

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

// => plasticine

也就是如果在thenonFulfilled回调中,返回新的Promise实例时,在构造函数中既调用了resolve又调用了reject的话,只会执行第一个被调用的,剩下的会被忽略,这个例子中reject就被忽略了

那么我们的MyPromise要如何实现这个功能呢?可以用一个布尔值进行标记,表示x.then中的两个回调是否有任何一个被调用过,有的话则不会再去执行它们

function resolvePromise(promise, x, resolve, reject) {
  // 根据 Promises A+ 规范 2.3.1: promise 和 x 指向同一个引用时应当 reject 一个 TypeError
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }

  // 2.3.3: x 是 object or function 时的情况
  let isCalled = false // 当 x 是 MyPromise 实例时,标记 x.then 中的回调是否有任何一个被调用过

  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // typeof null === 'object',因此需要额外判断一下排除 x 是 null 的情况

    try {
      // x.then 可能被 Object.defineProperty 设置了 getter 劫持,并抛出异常,因此要 try/catch 捕获
      let then = x.then

      if (typeof then === 'function') {
        // x 有 then 方法时,就将其视为是 MyPromise 对象
        // 2.3.3.3 执行 x.then,并且要显式绑定 this 指向,且有两个回调 resolvePromise 和 rejectPromise
        then.call(
          x,
          // resolvePromise
          (y) => {
            // 需要递归调用 MyPromise 中 resolve 出去的值,也就是这里的 y
            if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行

            isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 rejectPromise
            resolvePromise(promise, y, resolve, reject)
          },
          // rejectPromise
          (r) => {
            // 对于 reject,直接将其 reason 给 reject 出去即可
            if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行

            isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 resolvePromise
            reject(r)
          }
        )
      } else {
        // x 没有 then 方法 -- 不需要特殊处理,直接 resolve
        resolve(x)
      }
    } catch (e) {
      // 如果出错了的话,不应该还能调用 resolve,因此这里也要加上 isCalled 判断,防止去调用 resolve
      if (isCalled) return

      isCalled = true
      reject(e)
    }
  } else {
    // 基本数据类型 -- 直接 resolve
    resolve(x)
  }
}

4.5 功能测试

后面的规范就没什么了,现在可以来测试一下功能是否正常

const promise1 = new MyPromise((resolve, reject) => {
  resolve('plasticine')
})

const promise2 = promise1.then(
  (value) => {
    return new MyPromise((resolve, reject) => {
      resolve(value)
      reject(new Error('something wrong...'))
    })
  },
  (reason) => {
    return reason
  }
)

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

// => plasticine
const promise2 = promise1.then(
  (value) => {
    return new MyPromise((resolve, reject) => {
      // 异步调用 resolve
      setTimeout(() => {
        resolve(value)
      }, 2000)
    })
  },
  (reason) => {
    return reason
  }
)

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

// => plasticine

可以看到,和原生Promise的效果一致,在onFulfilled中返回了一个MyPromise的时候仍然能够在后面的then中拿到resolve出去的value

并且既调用了resolve又调用了rejectreject被忽略了,也和原生Promise表现一致,同时异步调用resolve也是能正常工作的


5. then回调是可选参数

Promises A+规范中定义了then的两个回调onFulfilledonRejected是可选的,也就意味着可以直接.then()调用

那么这样调用的时候我们应当将resolvevalue穿透传递下去,直到有then的回调onFulfilled去处理,因此我们可以给onFulfilledonRejected默认值

then(onFulfilled, onRejected) {
  // onFulfilled 和 onRejected 是可选参数,如果没传的时候应当设置默认值
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }
  // ...
}

测试一下then不传入回调的时候是否能够穿透

new MyPromise((resolve, reject) => {
  resolve('plasticine')
})
  .then()
  .then()
  .then()
  .then()
  .then((value) => {
    console.log(value)
  })

// => plasticine

确实可以!


6. 实现catch

真的实现完了吗?原生Promise对象还有一个catch方法呢!

事实上,catch做的事情就是then的第二个回调做的事情,都是用于处理reject出来的reason的,Promises A+中并没有规定catch这个方法,其实可以把catch方法看成是then的第二个回调的语法糖,也就是只传入了第二个回调:this.then(null, () => {})

那么我们直接复用then就可以实现catch

catch(onRejected) {
  return this.then(null, onRejected)
}

7. 测试是否符合Promises/A+规范

可以使用promises-aplus-tests这个工具来测试我们的MyPromise是否符合Promises/A+规范

首先安装该工具

pnpm i promises-aplus-tests -D

然后在MyPromise.js的底部添加如下代码,这段代码仅用作该工具测试使用,没有别的意义

// 用于测试 Promises/A+ 规范
MyPromise.defer = MyPromise.deferred = function () {
  let deferred = {}

  deferred.promise = new MyPromise((resolve, reject) => {
    deferred.resolve = resolve
    deferred.reject = reject
  })

  return deferred
}

接下来运行

npx promises-aplus-tests ./MyPromise.js

最后看到全部测试项目均通过! image.png


8. 修复bug -- 在executor中resolve一个MyPromise实例

虽然通过了Promise/A+规范的测试,但是实际上目前我们的代码是有一个bug的,看一下如下代码

const promise = new MyPromise((resolve, reject) => {
  resolve(
    new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve('resolved value')
      }, 2000)
    })
  )
})

promise.then((res) => {
  console.log(res)
})

// =>
// MyPromise {
//   status: 'PENDING',
//   value: undefined,
//   reason: undefined,
//   onFulfilledCallbackList: [],
//   onRejectedCallbackList: []
// }

当我们resolve一个MyPromise实例出去的时候,在这个MyPromise实例中异步resolve一个值出去,并不能够被then捕获到

事实上即便不是异步resolve,而是直接resolve出去也是不能被捕获到的,得到的只是一个处于PENDING状态的MyPromise实例

那如果我就是希望能够在then当中得到真正的value而不是初次被resolve出去的MyPromise实例该怎么办呢?因为原生的Promise就是这样的特性

我们可以对valueMyPromise实例的这种情况进行单独处理,调用它里面的then方法,将真正的valueresolve出去

const resolve = (value) => {
  // 当 value 是 MyPromise 实例的时候 --> 调用它的 then 方法
  if (value instanceof MyPromise) {
    value.then(resolve, reject)

    // 一定要 return 否则会无限递归下去
    return
  }
  // ...
}

**value**是一个**MyPromise**的时候一定要**return**退出**resolve**函数,否则会因为没有**base case**而无限递归下去

现在这个bug就修复好啦!

const promise = new MyPromise((resolve, reject) => {
  resolve(
    new MyPromise((resolve, reject) => {
      setTimeout(() => {
        resolve('resolved value')
      }, 2000)
    })
  )
})

promise.then((res) => {
  console.log(res)
})

// => resolved value

9. 完整代码

至此,整个Promise的功能就算是完整实现了,完整代码如下

/**
 * @description MyPromise 的状态
 */
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

/**
 * 根据 x 的类型作出不同的处理
 * x 为非 MyPromise 对象的时候 -- 直接 resolve
 * x 为 MyPromise 对象的时候 -- 调用其 resolve
 *
 * 剩下的还有一些细节会具体在代码中注释
 *
 * @param {MyPromise} promise MyPromise 实例
 * @param {any} x 前一个 MyPromise resolve 出来的值
 * @param {Function} resolve promise 对象构造函数中的 resolve
 * @param {Function} reject promise 对象构造函数中的 reject
 */
function resolvePromise(promise, x, resolve, reject) {
  // 根据 Promises A+ 规范 2.3.1: promise 和 x 指向同一个引用时应当 reject 一个 TypeError
  if (promise === x) {
    return reject(
      new TypeError('Chaining cycle detected for promise #<MyPromise>')
    )
  }

  // 2.3.3: x 是 object or function 时的情况
  let isCalled = false // 当 x 是 MyPromise 实例时,标记 x.then 中的回调是否有任何一个被调用过

  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // typeof null === 'object',因此需要额外判断一下排除 x 是 null 的情况

    try {
      // x.then 可能被 Object.defineProperty 设置了 getter 劫持,并抛出异常,因此要 try/catch 捕获
      let then = x.then

      if (typeof then === 'function') {
        // x 有 then 方法时,就将其视为是 MyPromise 对象
        // 2.3.3.3 执行 x.then,并且要显式绑定 this 指向,且有两个回调 resolvePromise 和 rejectPromise
        then.call(
          x,
          // resolvePromise
          (y) => {
            // 需要递归调用 MyPromise 中 resolve 出去的值,也就是这里的 y
            if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行

            isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 rejectPromise
            resolvePromise(promise, y, resolve, reject)
          },
          // rejectPromise
          (r) => {
            // 对于 reject,直接将其 reason 给 reject 出去即可
            if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行

            isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 resolvePromise
            reject(r)
          }
        )
      } else {
        // x 没有 then 方法 -- 不需要特殊处理,直接 resolve
        resolve(x)
      }
    } catch (e) {
      // 如果出错了的话,不应该还能调用 resolve,因此这里也要加上 isCalled 判断,防止去调用 resolve
      if (isCalled) return

      isCalled = true
      reject(e)
    }
  } else {
    // 基本数据类型 -- 直接 resolve
    resolve(x)
  }
}

class MyPromise {
  constructor(executor) {
    this.status = PENDING // 初始状态为 PENDING
    this.value = undefined // resolve 出去的值
    this.reason = undefined // reject 出去的原因

    // 维护两个列表容器 存放对应的回调
    this.onFulfilledCallbackList = []
    this.onRejectedCallbackList = []

    /**
     * resolve, reject 是两个函数 -- 每个 MyPromise 实例中的 resolve 方法是属于自己的
     * 如果将 resolve, reject 定义在构造器外面的话,方法会在构造函数的 prototype 上
     * 所以应当在构造器内部定义 resolve, reject 函数
     *
     * 并且一定要用箭头函数去定义 而不能是普通函数
     * 普通函数的 this 指向是在外部调用的时候决定的,不能保证指向实例本身
     * 而箭头函数中没有 this,会使用函数定义时的父函数 constructor 中的 this
     * 也就是说使用箭头函数能够保证 this 指向实例本身
     *
     * 而箭头函数要用 const 关键字声明一个引用变量去指向它,因此要在执行 executor 之前定义
     * 否则会由于暂时性死区导致无法找到 resolve, reject 函数
     */

    const resolve = (value) => {
      // 当 value 是 MyPromise 实例的时候
      if (value instanceof MyPromise) {
        value.then(resolve, reject)

        // 一定要 return 否则会无限递归下去
        return
      }

      // 只有在 PENDING 状态时才能转换到 FULFILLED 状态
      if (this.status === PENDING) {
        this.status = FULFILLED
        this.value = value

        // 触发 resolve 行为,开始 “发布” resolved 事件
        this.onFulfilledCallbackList.forEach((fn) => fn())
      }
    }

    const reject = (reason) => {
      // 只有在 PENDING 状态时才能转换到 REJECTED 状态
      if (this.status === PENDING) {
        this.status = REJECTED
        this.reason = reason

        // 触发 reject 行为,开始 “发布” rejected 事件
        this.onRejectedCallbackList.forEach((fn) => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    // onFulfilled 和 onRejected 是可选参数,如果没传的时候应当设置默认值
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : (value) => value
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }

    const promise2 = new MyPromise((resolve, reject) => {
      // 根据状态去判断执行哪一个回调
      if (this.status === FULFILLED) {
        // 以宏任务的方式执行 resolvePromise 才能保证拿到 promise2 实例
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            // 处理 x
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }

      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            // 处理 x
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }

      if (this.status === PENDING) {
        // pending 状态下需要 “订阅” fulfilled 和 rejected 事件
        this.onFulfilledCallbackList.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value)
              // 处理 x
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.onRejectedCallbackList.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              // 处理 x
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
      }
    })

    return promise2
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }
}

// 用于测试 Promises/A+ 规范
MyPromise.defer = MyPromise.deferred = function () {
  let deferred = {}

  deferred.promise = new MyPromise((resolve, reject) => {
    deferred.resolve = resolve
    deferred.reject = reject
  })

  return deferred
}

module.exports = MyPromise