手写一个符合 A+ 规范的 Promise

Promise
首页

之前也手写过简单的 promise,这次则是为了通过官方的 Promise A+ 测试集,借鉴了一些下载量较多的 promise polyfill,改了几遍,终于是通过了 A+ 规范的 872 个测试用例

如何测试?

测试库地址在这:promises-tests ,大家在写完自己的 promise 后,不妨也去测试一下,检验自己的 promise 是否符合 Promise A+ 规范。这个库使用起来很方便,像下面这样就可以了:

const tests = require('promises-aplus-tests')
const Promise = require('./index')

const deferred = function () {
  let resolve, reject
  const promise = new Promise(function (_resolve, _reject) {
    resolve = _resolve
    reject = _reject
  })
  return {
    promise: promise,
    resolve: resolve,
    reject: reject
  }
}
const adapter = {
  deferred
}
tests.mocha(adapter)

其中,index.js 中是你写的 Promise

实现

首先我们定义一些全局属性:

const IS_ERROR = {}
let ERROR = null

IS_ERROR作为发生错误时的标识,ERROR用来保存错误;

做好准备工作,再来定义_Promise 类,其中 fn 是 Promise 接受的函数,构造函数执行时立刻调用;_status 是 Promise 的状态,初始为 0(pending),resolved 时为 1,rejected 时为 2;_value 用来保存 Promise resolved 时的返回值和 rejected 时的失败信息;_handlers 用来保存 Promise 成功和失败时调用的处理方法

function _Promise(fn) {
  this._status = 0
  this._value = null
  this._handlers = []
  doFn(this, fn)
}

最后执行 doFn 方法,传入 this 值和 fn:

function doFn(self, fn) {
  const ret = safeCallTwo(
    fn,
    function (value) {
      self.resolve(value)
    },
    function (reason) {
      self.reject(reason)
    }
  )
  if (ret === IS_ERROR) {
    self.reject(ERROR)
  }
}

其中safeCallTwo是用来安全执行两参数方法的函数,当执行出错时,捕获错误,保存在ERROR中,返回IS_ERROR标识:

function safeCallTwo(fn, arg1, arg2) {
  try {
    return fn(arg1, arg2)
  } catch (error) {
    ERROR = error
    return IS_ERROR
  }
}

doFn中,调用safeCallTwo,fn 传入两个参数供我们调用,也就是我们常用的resolve方法和reject方法,并获取到返回值,如果 ret 为错误标识IS_ERROR,则调用 reject

_Promise 原型上挂载着 resolve 和 reject 方法,如下:

_Promise.prototype.resolve = function (value) {
  if (this._status !== 0) {
    return
  }
  this._status = 1
  this._value = value
  doThen(this)
}
_Promise.prototype.reject = function (reason) {
  if (this._status !== 0) {
    return
  }
  this._status = 2
  this._value = reason
  doThen(this)
}

因为 Promise 的状态只能由pending转为resolvedrejected,所以在执行 resolve 和 reject 方法时,要先判断 status 是否为 0,若不为 0,直接 return;修改 status 和 value 后,执行 doThen 方法:

function doThen(self) {
  const handlers = self._handlers
  handlers.forEach((handler) => {
    doHandler(self, handler)
  })
}

doThen 函数的作用是从 self 上取出的 handlers 并依次执行

我们再来看一看挂载在原型上的 then 方法:

_Promise.prototype.then = function (onResolve, onReject) {
  const res = new _Promise(function () {})
  preThen(this, onResolve, onReject, res)
  return res
}

我们知道,Promise 是支持链式调用的,所以我们的 then 方法也会返回一个 Promise,以供后续调用;

下面是 preThen 方法:

function preThen(self, onResolve, onReject, res) {
  onResolve = typeof onResolve === 'function' ? onResolve : null
  onReject = typeof onReject === 'function' ? onReject : null
  const handler = {
    onResolve,
    onReject,
    promise: res
  }
  if (self._status === 0) {
    self._handlers.push(handler)
    return
  }
  doHandler(self, handler)
}

preThen 方法接受 4 个值,分别为当前 Promise——self,resolve 后的回调函数 onResolve,reject 后的回调函数 onReject,then 函数返回的 promise——res。先判断 onResolve 和 onReject 是否为函数,若不是,直接置为 null。再将 onResolve、onReject、res 放入 handler 对象中

接下来需要注意,Promise 接受的函数(也就是上文的 fn)并不是一定是异步调用 resolve 和 reject,也有可能是同步的,也就是说在执行 preThen 函数时,self 的 status 可能已经不为 0 了,这时候我们就不需要将 handler 保存起来等待调用,而是直接调用回调函数

doHandler 函数代码见下:

function doHandler(self, handler) {
  setTimeout(() => {
    const { onReject, onResolve, promise } = handler
    const { _status, _value } = self
    const handlerFun = _status === 1 ? onResolve : onReject
    if (handlerFun === null) {
      _status === 1 ? promise.resolve(_value) : promise.reject(_value)
      return
    }
    const ret = safeCallOne(handlerFun, _value)
    if (ret === IS_ERROR) {
      promise.reject(ERROR)
      return
    }
    promise.resolve(ret)
  })
}

我们知道,即使是同步执行 relove 或者 reject,then 函数接受的回调函数也不会立刻同步执行,如下代码会依次输出 1,3,2,而非 1,2,3

const p = new Promise((resolve) => {
  console.log(1)
  resolve()
})
p.then(() => {
  console.log(2)
})
console.log(3)

在这里,我使用了 setTimeout 来模拟这种模式,当然,这只是一种粗糙的模拟,更好的方式是引入或实现类似 asap 的库(下个星期我可能会实现这个,哈哈),但 setTimeout 也足够通过测试了

doHandler 函数中,我们调用相应的回调函数,需要注意的是,如果相应回调函数为 null(null 是前文判断回调函数不为 function 时统一赋值的),则直接调用 then 函数返回的 promise 的 resolve 或 reject 方法。

同样,我们使用了 safeCallOne 来捕获错误,这里不再赘述

到这里,我们执行测试,发现不出意外地没有通过,因为我们只是实现了基础的 Promise,还没有实现 resolve 中的 thenable 功能,下面是 mdn 对于 thenable 的描述:

返回一个状态由给定value决定的Promise对象。如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用

我们再来修改 resolve 方法:

_Promise.prototype.resolve = function (value) {
  if (this._status !== 0) {
    return
  }
  if (this === value) {
    return this.reject(new TypeError("cant's resolve itself"))
  }
  if (value && (typeof value === 'function' || typeof value === 'object')) {
    const then = getThen(value)
    if (then === IS_ERROR) {
      this.reject(ERROR)
      return
    }
    if (value instanceof _Promise) {
      value.then(
        (value) => {
          this.resolve(value)
        },
        (reason) => {
          this.reject(reason)
        }
      )
      return
    }
    if (typeof then === 'function') {
      doFn(this, then.bind(value))
      return
    }
  }
  this._status = 1
  this._value = value
  doThen(this)
}

先判断 this 和 value 是否为一个 Promise,若是一个,则抛出错误

再判断 value 的类型是否为 function 或 object,如果是,则实行 getThen 方法进行错误捕获:

function getThen(self) {
  try {
    return self.then
  } catch (error) {
    ERROR = error
    return IS_ERROR
  }
}

若成功拿到 then 方法,检测value instanceof _Promise,若为 true,则直接采用 value 的状态和 value 或者 reason。

若 then 为 function,则将 then 函数以 value 为 this 值,当作 fn 执行,也就是达成下面代码的效果:

const p = new Promise((resolve) => {
  resolve({
    then: (_resolve) => {
      _resolve(1)
    }
  })
})
p.then((value) => console.log(value)) //打印1

我们再次执行测试,发现仍然有错,其因出现在下面这种情况下:

const p = new _Promise((resolve) => {
  resolve({
    then: (_resolve) => {
      setTimeout(() => _resolve(1)), 500
    }
  })
  resolve(2)
})
p.then((value) => console.log(value))

这个时候,使用我们的 Promise,输出的是 2,而在规范中,应当是输出 1

原因是我们在对象的 then 方法中是异步地 resolve,这个时候,下面的resolve(2)在执行时,status 还没有变,自然可以修改 status 和 value

解决方法也很简单,只用在 doFn 方法中判断是否为第一次执行即可:

function doFn(self, fn) {
  let done = false
  const ret = safeCallTwo(
    fn,
    function (value) {
      if (done) {
        return
      }
      done = true
      self.resolve(value)
    },
    function (reason) {
      if (done) {
        return
      }
      done = true
      self.reject(reason)
    }
  )
  if (ret === IS_ERROR) {
    if (done) {
      return
    }
    done = true
    self.reject(ERROR)
  }
}

再执行测试,发现已经测试用例全部通过~ 4BCA9C53-AC78-4BC4-880B-9FAB820A3074

代码

完整代码已放在我的 github 上,地址为https://github.com/Bowen7/playground/tree/master/promise-polyfill ,可以 clone 我的 playground 项目,再到 promise-polyfill 目录下npm install,然后执行npm test即可运行测试