qinjunyi / blog

工作实践总结 and 学习笔记
MIT License
0 stars 0 forks source link

基于A+规范的Promise实现 #2

Open qinjunyi opened 3 years ago

qinjunyi commented 3 years ago

基于 A+规范的 Promise 实现

前言

JavaScript 作为单线程编程语言,在异步流程控制方面有其独特的解决方案,且更新换代的速度也是非常快,从最原始的回调函数,到 Promise、Generator 以及目前实际开发中常用的 async/await 语法糖,不管是易用性还是代码可读性都有了很大的提升。本文旨在参考Promise/A+规范(手动撸一个 Promise 前建议先浏览规范)实现一个基本满足规范要求的 Promise。什么是 Promise?建议参考阮一峰老师的es6 入门


骨架

Promise 构造函数接受一个可执行函数的参数,此函数有两个形参 resolve 和 reject,所接受的实参类型都是 Function。resolve 用来处理 Promise 正常返回时的值,reject 用来处理各类异常值,其定义以及实现是在 Promise 构造函数中,实例化过程中这个可执行函数会被执行。要注意的是,该函数执行要放在 try/catch 中以便异常捕获。

function MyPromise(executor) {
  function resolve() {}
  function reject() {}
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
}

状态控制

Promise 对象内部有三个状态 pending,fulfilled,rejected,通俗地说依次就是正在进行中,成功,失败。规范约定了初始状态为 pending,只有 Promise 保存的异步操作的结果才能决定内部的最终状态,其他任何操作都不能改变该状态,且该状态一经改变就不会再变了。状态改变只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。因此可声明三个常量来存储这三种状态。

// ./src/constants/status.constants.js
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

//./index.js
function MyPromise(executor) {
  this.status = status.PENDING
  function resolve() {
    if (self.status === status.PENDING) {
      this.status = status.FULFILLED
    }
  }
  function reject() {
    if (self.status === status.PENDING) {
      this.status = status.REJECTED
    }
  }
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
}

异步结果处理

当异步操作完成,过程成功或失败时需要有变量来存储结果,因此可以声明 value 来存储成功值,reason 来存储失败时的原因

function MyPromise(executor) {
  let self = this
  self.status = status.PENDING
  self.value = null
  self.reason = null
  function resolve(value) {
    if (self.status === status.PENDING) {
      self.value = value
      self.status = status.FULFILLED
    }
  }
  function reject(reason) {
    if (self.status === status.PENDING) {
      self.reason = reason
      self.status = status.REJECTED
    }
  }
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
}

.then()方法

规范约定了 Promise 有一个.then()方法,该方法有两个形参 onFulfilled,onRejected,二者都是 Function 类型。前者是在状态变为 fulfilled 时触发,接受存储成功结束异步操作结果的 value 作为参数,后者是在状态变为 rejected 时触发,接受异步操作有抛异常结果的 reason 作为参数。以此为据,实现一个基础版的 then 方法

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  let self = this
  if (self.status === status.FULFILLED) {
    onFulfilled(self.value)
  }
  if (self.status === status.REJECTED) {
    onRejected(self.reason)
  }
}

基础版 then 方法,其实只处理了同步改变状态的场景,若内部状态处于 pending,结束异步操作是在下一个 loop,那么在执行 then 时 self.value 或 self.reason 为 null,且 onFulfilled 和 onRejected 都没有触发时机。因此可利用订阅发布的思想,声明 onResolvedCallbacks 和 onRejectedCallbacks 两个数组来进一步完善 then 和 Promise 的构造函数

function MyPromise(executor) {
  let self = this
  self.status = status.PENDING
  self.value = null
  self.reason = null
  self.onResolvedCallbacks = []
  self.onRejectedCallbacks = []
  function resolve(value) {
    if (self.status === status.PENDING) {
      self.value = value
      self.status = status.FULFILLED
      //状态改变代表异步操作结束,执行对应数组中订阅的回调函数
      self.onResolvedCallbacks.forEach((onResolvedCallback) => {
        onResolvedCallback()
      })
    }
  }
  function reject(reason) {
    if (self.status === status.PENDING) {
      self.reason = reason
      self.status = status.REJECTED
      //同resolve
      self.onRejectedCallbacks.forEach((onRejectedCallback) => {
        onRejectedCallback()
      })
    }
  }
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  let self = this
  //未结束异步操作时先订阅
  if (self.status === status.PENDING) {
    self.onFulfilledCallbacks.push(() => {
      onFulfilled(self.value)
    })
    self.onRejectedCallbacks.push(() => {
      onRejected(self.reason)
    })
  }
  if (self.status === status.FULFILLED) {
    onFulfilled(self.value)
  }
  if (self.status === status.REJECTED) {
    onRejected(self.reason)
  }
}

规范 2.2.7 小节描述 then 需要返回一个 promise,且对 onFulfilled 和 onRejected 对返回结果处理上也有要求。为了更加语义化,返回的 promise 命名为 returnPromise,onFulfilled 和 onRejected 的返回结果分别赋值给 success 和 failed 变量。规范要求 onFulfilled 和 onRejected 正常执行时返回的值要交给一个Promise Resolution Procedure(Promise决议程序) [[Resolve]](promise2, x)处理(先不关心这个决议程序具体实现,暂且声明其为 resolvePromise),若有抛出异常 e 则 returnPromise 被 rejected,且以 e 作为 reason。

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  let self = this
  let returnPromise = new MyPromise((resolve, reject) => {
    //未结束异步操作时先订阅
    if (self.status === status.PENDING) {
      self.onFulfilledCallbacks.push(() => {
        try {
          let success = onFulfilled(self.value)
          resolvePromise(returnPromise, success, resolve, reject)
        } catch (err) {
          reject(err)
        }
      })
      self.onRejectedCallbacks.push(() => {
        try {
          let failed = onRejected(self.reason)
          resolvePromise(returnPromise, failed, resolve, reject)
        } catch (err) {
          reject(err)
        }
      })
    }
    if (self.status === status.FULFILLED) {
      try {
        let success = onFulfilled(self.value)
        resolvePromise(returnPromise, success, resolve, reject)
      } catch (err) {
        reject(err)
      }
    }
    if (self.status === status.REJECTED) {
      try {
        let failed = onRejected(self.reason)
        resolvePromise(returnPromise, failed, resolve, reject)
      } catch (err) {
        reject(err)
      }
    }
  })
  return returnPromise
}

Promise Resolution Procedure(Promise 决议程序)

规范 2.3 针对 resolvePromise 进行了详细的描述,贴上该小节的一个中文版

Promise Resolution Procedure 是以一个promise和一个value为输入的抽象操作,我们将其表示为[[Resolve]](promise, x)。如果 x 是一个 thenable 且行为有几分像promise时,Promise Resolution Procedure 会试图让 promise 变为与 x 相同的状态。否则,promise 会被决议程序以 x 为值 fulfilled。

这种处理 thenable 的方式可以使不同的promise实现之间可以互操作,只要这些实现对外暴露了遵循 Promises/A+规范 的 then 方法即可。这种方式也使得遵循 Promises/A+规范 的实现可以“同化”那些没有遵循 Promises/A+规范 但具备合理的 then 方法的promise实现。

执行 [[Resolve]](promise, x) 需运行以下步骤:

2.3.1. 如果 promise 和 x 指向同一个对象,则 reject promise 并且以 TypeError 作为 reason。

2.3.2. 如果 x 是一个promise,则采用 x 的状态 [详情见3.4]:

    2.3.2.1. 如果 x 处于 pending 状态,则 promise 必须保持 pending 状态直到 x 被 fulfilled 或 rejected。

    2.3.2.2. 当 x 变为或已处于 fulfilled 时,用相同的 value 来 fulfill promise。

    2.3.2.3. 当 x 变为或已处于 rejected 时,用相同的 reason 来 reject promise。

2.3.3. 如果 x 是一个对象或函数,

    2.3.3.1. 令 then 指向 x.then。[详情见3.5]

    2.3.3.2. 如果访问 x.then 属性导致抛出异常 e,则以 e 为 reason 来 reject promise。

    2.3.3.3. 如果 then 是一个函数,则调用它,并且以 x 作为 this,以 resolvePromise 作为第一个参数,以 rejectPromise 作为第二个参数,且:

        2.3.3.3.1. 如果或当 resolvePromise 以一个值 y 被调用了,则执行 [[Resolve]](promise, y)。

        2.3.3.3.2. 如果或当 rejectPromise 以一个值 r 被调用了,则以 r 来 reject promise。

        2.3.3.3.3. 如果 resolvePromise 和 rejectPromise 都被调用了,或者某个已被被多次调用了,则首次发生的调用生效,其余所有调用都被忽略。

        2.3.3.3.4. 如果调用 then 抛出异常 e:

            2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已经被调用过了,则忽略这个异常。

            2.3.3.3.4.2 否则,以 e 来 reject promise。

    2.3.3.4. 如果 then 不是函数,则以 x 来 fulfill promise。

2.3.4. 如果 x 既不是对象也不是函数,则以 x 来 fulfill promise。

如果一个promise被一个 thenable 决议,且此 thenable 在一个循环的thenable链中,[[Resolve]](promise, thenable) 的递归特性会导致 [[Resolve]](promise, thenable) 再次被调用,根据上面的算法,这会导致无限递归。因此我们鼓励(但不强制要求)promise实现时能检测无限递归,并在出现无限递归时以一个 TypeError 来 reject promise。

对照该小节的要求实现 resolvePromise

function resolvePromise(promiseObj, result, resolve, reject) {
  //对应2.3.3.3.3小节,做一个防御防止多次改变内部状态
  let called = false
  if (promiseObj === result) return reject(new TypeError('循环引用'))
  if (result instanceof MyPromise) {
    // 如果result的状态还没有确定,那么它是有可能被一个thenable决定最终状态和值的
    // 所以这里需要做一下处理,而不能一概的以为它会被一个“正常”的值resolve
    if (result.status === status.PENDING) {
      result.then((val) => {
        resolvePromise(promiseObj, val, resolve, reject)
      }, reject)
    } else {
      // 但如果这个Promise的状态已经确定了,那么它肯定有一个“正常”的值,而不是一个thenable,所以这里直接取它的状态
      result.then(resolve, reject)
    }
  } else if (
    Object.prototype.toString.call(result) === '[object Object]' ||
    Object.prototype.toString.call(result) === '[object Function]'
  ) {
    let then = result.then
    if (typeof then === 'function') {
      then.call(
        result,
        (val) => {
          if (called) return
          called = true
          // 对应2.3.3.3.1正常结束异步操作后resolve的val有可能还是promise或者是具有then方法等,再次resolvePromise,直到该值为基本类型或者非thenable
          resolvePromise(promiseObj, val, resolve, reject)
        },
        (reason) => {
          if (called) return
          called = true
          reject(reason)
        }
      )
    } else {
      if (called) return
      called = true
      resolve(result)
    }
  } else {
    resolve(result)
  }
}

.then()保持异步执行

规范 3.1 中有提到 then 需要保持异步执行,且属于 js 执行机制 event loop 中的为微任务(micro-task),本文中为了方便直接用宏任务 setTimeout 代替实现

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  let self = this
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }
  let returnPromise = new MyPromise((resolve, reject) => {
    if (self.status === status.PENDING) {
      self.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let success = onFulfilled(self.value)
            resolvePromise(returnPromise, success, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)
      })
      self.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let failed = onRejected(self.reason)
            resolvePromise(returnPromise, failed, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)
      })
    }
    if (self.status === status.FULFILLED) {
      setTimeout(() => {
        try {
          let success = onFulfilled(self.value)
          resolvePromise(returnPromise, success, resolve, reject)
        } catch (err) {
          reject(err)
        }
      }, 0)
    }
    if (self.status === status.REJECTED) {
      setTimeout(() => {
        try {
          let failed = onRejected(self.reason)
          resolvePromise(returnPromise, failed, resolve, reject)
        } catch (err) {
          reject(err)
        }
      }, 0)
    }
  })
  return returnPromise
}

完善

构造函数中的 resolve

一个实例 resolve 时接受的 value 参数若也是 Promise 那么本实例的内部状态改变会受 value 是否已经改变内部状态的影响,因此应该交给 then 处理

//MyPromise
...
function resolve(value) {
    if (value instanceof MyPromise) {
      value.then(resolve, reject)
    }
    if (self.status === status.PENDING) {
      self.status = status.FULFILLED
      self.value = value
      self.onResolvedCallbacks.forEach((onResolvedCallback) => {
        onResolvedCallback()
      })
    }
  }
...

onFulfilled 和 onRejected 默认值处理

在实际应用中,会有如下情况

new Promise((resolve) => resolve('ok'))
  .then()
  .catch()
  .then(function (val) {
    console.log(val)
  })

即 then 不传 onFulfilled 和 onRejected,但发现打印的日志仍能显示ok,可见 onFulfilled 和 onRejected 是有默认值的,这其实也是 Promise 值穿透的特性。上述代码等同于

new Promise((resolve) => resolve('ok'))
  .then(function (val) {
    return val
  })
  .catch(function (reason) {
    throw reason
  })
  .then(function (val) {
    console.log(val)
  })

完善 then 方法,给 onFulfilled 和 onRejected 赋默认值

MyPromise.prototype.then = function (onFulfilled, onRejected) {
...
    onFulfilled =
        typeof onFulfilled === 'function' ? onFulfilled : (value) => value
    onRejected =
        typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
            }
...
}

原型上的方法

原生 Promise 原型上还有很多方法,此处简单介绍几种

catch

在 then 以及 resolvePromise 所有处理异步操作结果的同步代码都写在了 try/catch 中,若有抛出异常都会被 returnPromise 的 reject 捕获,因此 catch 其实就是一个特殊的 then,通过这个特殊的 then 的 onRejected 捕获 reject 的错误或前面 Promise 的 onRejected 默认值 throw 的 reason

MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected)
}

cancelable

实际开发中会有需求取消一个 Promise 的执行,可以借助一个辅助函数以及 Promise.race()来实现。外部可能通过执行 helper.cancel()提前改变状态以此“取消”promiseObj 的执行

MyPromise.prototype.cancelable = function (promiseObj, helper) {
  return MyPromise.race([
    promiseObj,
    new MyPromise((resolve, reject) => {
      helper.cancel = () => {
        reject(new TypeError('promise cancel'))
      }
    })
  ])
}

stop

cancelable 是间接取消某个 Promise 执行,那如何取消一个 Promise 链式调用?出发点是只要返回一个永远处于 pending 状态的 Promise 就不会继续向下执行 then 或 catch 了,也就做到了停止链式调用

MyPromise.prototype.stop = function () {
  return new MyPromise(() => {})
}

但也有缺点,所有链式调用的回调函数无法被垃圾回收了,造成内存资源浪费

done

then 方法中的 onFulfilled 是在 try/catch 中执行的,错误会被 catch 到,但是若后续没有 then 或 catch 了,这个错误无法被捕获,就没有任何异常,换种说法即Promise 有可能会吃掉错误
实现 done 方法作为 Promise 链式调用的最后一步,用来向全局抛出没有被 Promise 内部捕获的错误,并且不再返回一个 Promise,来处理错误被吃掉的问题

MyPromise.prototype.done = function () {
  return this.catch((reason) => {
    throw reason
  })
}

至此,一个基本符合规范的 Promise 已经实现,原型上还有一些常用的方法,例如 all、race、resolve、reject 等,在此不一一列出,可参考实现源码


完整代码


参考规范

Promise/A+规范