按照A+规范手写Promise

7 分钟阅读

掌握Promise大概是每个前端都必不可少的一项技能,Promise的设计初衷就是解决著名的 回调地狱(callback hell) 问题
Promise 是异步编程的一种解决方案,比传统的解决方案 回调函数和事件 更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
现如今的面试中手写代码成了一项必不可少的技能,我们今天就是按照Promise/A+的规范使用ES6的class来自己实现一个Promise类
按照A+的规范 一个Promise对象需要有三种状态 分别是 pending fulfilledrejected 代表promise的发送状态 成功状态和拒绝状态 需要有resolve和reject两个函数来处理对应状态 需要有value 和 reason来保存promise的值和失败的原因
有两个对应数组保存promise处理对应状态的回调函数 所以我们的Promise开始应该是这样的

text
/**1
 * 判断传入参数是否为function 辅助函数
 * @param {any} variable 要判断的参数
 * @returns {Boolean} 返回一个布尔值 true为函数 false不是函数
 */
const isFunction = variable => typeof variable === 'function'

class MyPromise {
 /**
   * @param {function} executor 处理Promise的handler 传入一个函数 
   函数有两个参数分别处理fulfilled和rejected状态
   */
  constructor(executor) {
    this.state = 'pending' // 当前promise的状态 一旦被更改后将不可再次更改
    this.value = undefined // promise的值
    this.reason = undefined // promise被拒绝的原因
    this.onResolvedCallbacks = [] // 成功回调的队列
    this.onRejectedCallbacks = [] // 失败回调的队列
     /**
     * Promise的resolve函数 处理成功的状态
     * @param {any} value Promise的值
     */
    let resolve = value => {
      if (this.state === 'pending') { // 只有在pending状态下才可以进入resolve 否则不作处理
        this.state = 'fulfilled' // 更改promise状态
        this.value = value 
        this.onResolvedCallbacks.forEach(fn => fn(value)) // 处理回调
      }
    }
   /**
     * Promise的reject函数 处理拒绝的状态
     * @param {any} reason Promise被拒绝的原因
     */
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn(reason))
      }
    }
    try {
      executor(resolve, reject) // 这里为了预防出现错误 使用了try catch
    } catch (err) {
      reject(err)
    }
  }
}

这就是一个Promise的雏形 下面我们根据规范来一步一步的完善 Promise必须要有一个then方法来获取成功或者失败的原因 then方法接受两个参数来处理对应状况 即promise.then(onFulfilled, onRejected) 这两个参数都是可选的 但是必须是一个函数

text
function resolvePromise(promise, arg, resolve, reject) {
  if (arg === promise) { // 处理循环引用
     return reject(new TypeError('Promise循环引用'));
  }
  let called // 这个变量用来标识promise是否已经resolve 或reject了
  if (arg != null && (typeof arg === 'object' || typeof arg === 'function')) { // 判断上次的返回值是否是一个对象或者函数
    try { // 我们默认他是一个thenable的对象或者函数 如果不是进入catch处理
      let then = arg.then
      if (typeof then === 'function') { // 
        then.call(
          arg,
          onResolve => {
            if (called) return //如果promise已经成功或失败了,则不会再处理了
            called = true
            resolvePromise(promise, onResolve, resolve, reject)
          },
          err => {
            if (called) return
            called = true
            reject(err)
          }
        )
      } else {
        resolve(arg) //arg不是一个thenable对象,那直接把它当成值resolve就可以了
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(arg)
  }
}
then(onFulfilled, onRejected) {
    // 当没有传入参数或者传入的不是一个函数时 我们直接给一个默认的函数 这是Promise实现值穿透的原理
    onFulfilled = isFunction(onFulfilled) 
      ? onFulfilled 
      : value => value
    onRejected = isFunction(onRejected)
      ? onRejected
      : err => {
          throw err
        }
    // then方法要返回一个新的Promise对象 实现链式调用
    let newPromise = new MyPromise((resolve, reject) => {
      // 成功状态的处理逻辑
      if (this.state === 'fulfilled') {
        setTimeout(() => {
        /* 这里我们只能用setTimeout来实现异步 其实是无法完全达到原生的Promise的效果的
        * (原生的Promise是微任务)但是由于这些是底层控制的 所以我们只能这么实现 稍后也会举例说明这么写的问题在哪里
        */
          try {
            let arg = onFulfilled(this.value)
            resolvePromise(newPromise, arg, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      if (this.state === 'rejected') { // 逻辑类似 就不多做赘述了
        setTimeout(() => {
          try {
            let arg = onRejected(this.reason)
            resolvePromise(newPromise, arg, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      if (this.state === 'pending') { //当处于pending状态下 直接把传入的回调函数推进队列中 等待状态改变后调用
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let arg = onFulfilled(this.value)
              resolvePromise(newPromise, arg, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let arg = onRejected(this.reason)
              resolvePromise(newPromise, arg, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
      }
    })
    return newPromise
  }

至此 我们的Promise已经基本可以正常使用了 不过我们还可以再继续的完善一下 Promise还有两个很重要的方法 catch和finally
catch和finally的实现都很简单 catch其实就是then方法没有传递onFulfilled参数 只处理出现错误的情况 虽然我们可以用then来处理 但是为了代码的清晰和规范 我们最好使用catch来捕获错误 而finally方法其实就是我们不用关心当前Promise的状态 不管处于什么状态 我们都会去执行的一个回调 这里有很多人对finally有一些误解 认为finally是会在Promise的最后来执行 其实并不是这样的 他只是一个用来处理不管我们Promise成功或者失败都会执行的逻辑的方法

text
 catch(fn) {
    return this.then(null, fn)
  }
  finally(cb) {
    return this.then(
      value => MyPromise.resolve(cb()).then(() => value),
      reason =>
        MyPromise.resolve(cb()).then(() => {
          throw reason
        })
    )
  }

这里我们的finally里使用了MyPromise中的静态方法 可能大家都没有看到如何实现的 这里连同resolve reject race 和 all的静态方法的实现也一并贴上

text
static resolve(value) {
// 如果传入参数是一个Promise实例或者一个thenable的参数就直接返回传入参数 否则包装成一个新的Promise返回
    return value instanceof MyPromise || (value && isFunction(value.then)) 
      ? value
      : new MyPromise(resolve => resolve(value))
  }
  static reject(value) {
    return new MyPromise((resolve, reject) => reject(value)) //直接返回一个拒绝状态的Promise对象
  }
  static race(promises) {
    return new MyPromise((resolve, reject) => {
      for (let p of promises) {
        // 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
        this.resolve(p).then(res => {
          resolve(res)
        }, reject)
      }
    })
  }
  //all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
  static all(promises) {
    return new MyPromise((resolve, reject) => {
      let values = [],
        count = promises.length
      for (let i in promises) {
        // 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
        this.resolve(promises[i]).then(res => {
          values[i] = res
          // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
          --count < 1 && resolve(values)
        }, reject)
      }
    })
  }

至此我们的Promise就完整的实现了 我们可以使用promises-aplus-tests库来测试我们的Promise 具体如何测试大家可以自行的百度一下
然后就是给大家讲一下文中提到的使用setTimeout来实现的一些和原生Promise有出入的地方和一些要注意的问题
之所以说使用setTimeout和原生的Promise有差距是因为在底层的实现上 Promise是微任务 而setTimeout是宏任务 这就造成了我们的代码执行顺序上会有不可避免的偏差 例如如下例子

text
setTimeout(function() {
  console.log(1)
}, 0)
new Promise(function(resolve) {
  console.log(2)
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve()
  }
  console.log(3)
}).then(function() {
  console.log(4)
})
console.log(5)

在使用原生Promise的情况下 执行完第一个宏任务之后 会立即执行所有微任务 当微任务队列被清空后 才会执行下一个宏任务 所以上面的代码会输出 2 3 5 4 1 而我们使用setTimeout实现时 由于都是宏任务 会按照插入队列的顺序执行 所以这时候会输出 2 3 5 1 4 这就是我们要注意的地方 如果对于宏任务和微任务不太了解 大家可以去看一下这篇文章这一次,彻底弄懂 JavaScript 执行机制 不过好在我们只是想通过这个来掌握Promise的实现原理 大家在日常开发中还是使用原生的Promise就好
另外一个问题就是如果Promise.all这个方法中传入的Promise参数中有自己的catch 那么只有在参数的catch方法中直接返回一个失败状态的promise或者throw一个错误 否则这个promise会显示为成功状态 不会影响Promise.all的执行 并且Promise.all的catch方法无法捕获到错误
所以如果需要使用Promise.all 那么尽量不要给你的参数添加catch 否则可能会达不到你的预期效果
本文实现的Promise的源码放在了我的Github 喜欢的话恳请各位大佬给个Star

相关文章