«
实现Promise

时间:2022-5   


要实现一个 Promise,首先要看的就是 Promise 标准,这个标准指的是 Promises/A+ 标准,建议在实现之前首先通读整个标准,正文内容不多,但对于英文不好的同学来说可能略显晦涩,这里推荐一个我个人觉得比较好的中文翻译版本

另外在开始写代码之前,我们应该先建立一个测试环境,在开发过程中经常跑一下测试用例,可以及时发现自己的错误。Promises/A+ 有自己的测试用例仓库,我们可以通过 npm 安装测试用例作为依赖。

根据文档,使用这个测试用例我们首先需要实现一个 adapter,用来将自己的实现连接到测试用例,其中我们需要在 adapter 中导出以下几个方法:

每个方法的作用请自行阅读文档,那么我们先来搭建这个框架,Promise 本身作为一个类,我们用 class 来实现就好:

// Promise.js
export default class Promise {}

Promise.resolve = function(value) {}
Promise.rejected = function(reason) {}

在 adapter.js 文件中,引入这个类,并导出测试用例需要的几个方法:

// adapter.js
import Promise from './Promise'

exports.resolved = function (value) {
  return Promise.resolve(value)
}

exports.rejected = function (reason) {
  return Promise.rejected(reason)
}

exports.deferred = function () {
  let resolveFn, rejectFn
  const promise = new Promise((resolve, reject) => {
    resolveFn = resolve
    rejectFn = reject
  })
  return {
    promise,
    resolve: resolveFn,
    reject: rejectFn
  }
}

根据命令行,运行这个测试用例有三种方法,分别是通过 CLI 的方式、通过 Node 运行脚本的方式以及在 Mocha 测试套件中运行,前两种方法比较简单,不用额外安装其他依赖,选择哪种都可以。

分析标准内容

在动手写之前,先来分析一下标准的内容,大致可以分为以下几个方面,(以下内容如果第一次阅读时不能完全理解,可以先看代码实现部分,然后结合实现来理解标准):

Promise 对象的状态:

2.1 章节描述了 Promise 对象的状态变更策略,只需要注意 Promise 对象的状态由 pending 转变为 fulfilled 或 rejected 的过程是不可逆的即可

then  方法

2.2 章节描述了 Promise 对象提供的  then  方法的特征,要注意的点有以下几个:

  1. 接受两个函数参数,分别是 onFulfilled 和 onRejected,他们分别作为 Promise 对象在 fulfilled 和 rejected 两个状态时的回调,如果 Promise 对象的状态为 pending,则先将回调缓存至队列,并在 Promise 对象发生状态变化时,在下一个事件循环中依次执行响应队列的中的回调。
  2. then 方法应该返回一个新的 promise。
  3. Promise 对象的 onFulfilled 回调和 onRejected 回调如果返回一个值 x,则在 then 方法返回的新 promise 上用 x 执行解决程序(记为 [[Resolve]](promise, x) ,下同)。如果回调抛出异常,则新 promise 被 reject。

解决程序

上一段提到的解决程序在标准的 2.3 章节进行了详细的解释。所谓的  [[Resolve]](promise, x)  的含义是指用 x 的值来解决  promise  的过程,其中遵循的规则我归纳了一下,如下:

  1. promise 和 x 不能指向同一个对象,否则抛出异常。
  2. 如果 x 是一个 promise ,则 promise 的状态遵循 x 的状态变更。
  3. 如果 x 不是一个函数或对象,就用 x 来 fulfill 这个 promise
  4. 如果 x 是一个函数或对象,尝试获取 x.then ,根据不同情况继续分类:

分类如下:

  1. 如果获取 x.then 过程抛出异常,则将 promise reject 掉。
  2. 如果 then 是一个函数,以 x 作为 this 来执行它,参数分别为 resolvePromise 和 rejectPromise,如果 resolvePromise 被执行且参数为 y,执行 [[Resolve]](promise, y),如果 rejectPromise 被执行且参数为 r,将 promise reject 掉。resolvePromise 和 rejectPromise 两个函数是竞态的,且都只能执行一次。
  3. 执行 then 的过程抛出异常时,如果 resolvePromise 和 rejectPromise 还未被执行,就忽略异常,否则,将 promise reject 掉。
  4. 如果 x.then 不是函数,将 promise 变更为 fulfilled。

Promise 的实现

下面就可以开始动手实现了,首先按照标准来完成 Promise 的状态变更行为:

const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

class Promise {
  constructor(executor) {
    // 保存promise的状态,默认为pending
    this.status = PENDING
    // 保存promise的值,用作fulfilled回调的参数传递
    // 由于promise状态只能是fulfilled或rejected两者之一,因此可以用一个属性来保存value或reason
    this.value = null

    // 执行此函数时,promise的状态可由pending变为fulfilled
    let resolve = value => {
      if (this.status !== PENDING) {
        return
      }
      this.status = FULFILLED
      this.value = value
    }

    // 执行此函数时,promise的状态可由pending变为rejected
    let reject = reason => {
      if (this.status !== PENDING) {
        return
      }
      this.status = REJECTED
      this.value = reason
    }

    try {
      // 执行传入Promise构造函数的函数,将resolve和reject作为两个参数传入
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }
}

另外,在之前的 adapter.js 文件中用到的两个 Promsie 类上的静态方法可以一并实现一下:

Promise.resolve = function (value) {
  return new Promise((resolve) => {
    resolve(value)
  })
}

Promise.rejected = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
}

下面来初步实现一下  then  方法,标准里说到, then  方法的两个函数类型的参数应作为 promise 状态为 fulfilled 和 rejected 时的回调,同时promise的  value  属性应作为回调的参数传入,且回调函数应在下一个事件循环中执行,具体实现不限于宏任务或者微任务,我们可以用最常见的  setTimeout  来做:

class Promise {
    // 其他代码省略...

    then(onFulfilled, onRejected) {
    const promise = new Promise((resolve, rejected) => {
      // fulfilled状态执行onFulfilled回调
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            const ret = onFulfilled(this.value)
            resolve(ret)
          } catch (err) {
            reject(err)
          }
        }, 0)
      }
      // rejected状态执行onRejected回调
      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let ret = onRejected(this.value)
            resolve(ret)
          } catch (err) {
            rejected(err)
          }
        }, 0);
      }
    })
    return promise
  }
}

下面我们来试验一下能否实现基本的功能,可以用下面的测试代码:

new Promise((resolve) => {
  console.log('inner')
  resolve('fulfilled')
}).then((value) => console.log(value))
console.log('outer')
// 输出:
// inner
// outer
// fulfilled

new Promise((resolve, reject) => {
  console.log('inner')
  reject('rejected')
}).then(
  (value) => console.log(value),
  (reason) => console.log(reason)
)
console.log('outer')
// 输出:
// inner
// outer
// rejected

上面实现的是当 promise 已经不是 pending 状态时调用  then  方法的行为,但在实际应用中更常见的是调用  then  方法时 promise 仍然为 pending 状态,传入  then  的回调需要在未来某个时间点被执行。

这里可以思考一下,如果要在 promise 状态被改变时执行回调,那么首先我们需要保存这些回调函数,同时应在  resolve  和  reject  函数中触发执行这些回调的逻辑,换言之需要在  resolve  和  reject  中能够访问到这些回调。比较容易想到的就是在 Promise 类上新增两个队列用于分别保存两种回调函数,这样  resolve  和  reject  函数也能在所在的作用域中访问到这两个队列,下面新增一些代码:

class Promise {
  constructor(executor) {
    // 新增
    // 用于存放fulfilled回调
    this.onFulfilledCallbacks = []
    // 用于存放rejected回调
    this.onRejectedCallbacks = []

    let resolve = value => {
      // 新增
      // 被resolve后依次执行回调
      setTimeout(() => {
        for (let i = 0, len = this.onFulfilledCallbacks.length; i < len; i++) {
          this.onFulfilledCallbacks[i].call(null, this.value)
        }
        // 清空队列
        this.onFulfilledCallbacks = []
      })
    }

    let reject = reason => {
      // 新增
      setTimeout(() => {
        for (let i = 0, len = this.onRejectedCallbacks.length; i < len; i++) {
          this.onRejectedCallbacks[i].call(null, this.value)
        }
        // 清空队列
        this.onRejectedCallbacks = []
      })
    }
  }
}

这样  resolve  和  reject  两个函数就基本实现完毕了,下面需要给  then  方法增加添加回调函数到队列中的操作,另外当传入的  onFulfilled  和  onRejected  不是函数时,要支持将 value 和 reason 透传到下一个 promise,在开头加入判断即可:

class Promise {
  // 省略部分代码
  then() {
    let promise = new Promise((resolve, reject) => {
      if (typeof onFulfilled !== 'function') {
        // 构造一个onFulfilled函数,传递value到下一个then
        onFulfilled = (value) => resolve(value)
      }
      if (typeof onRejected !== "function") {
        // 构造一个onRejected函数,传递reason到下一个then
        onRejected = (reason) => reject(reason);
      }
      // 省略部分代码
      if (this.status === PENDING) {
        // 将onFulfilled回调包裹一层,以便执行onFulfilled如抛出异常可以reject当前promise
        this.onFulfilledCallbacks.push((value) => {
          try {
            let ret = onFulfilled(value)
            resolve(ret)
          } catch (err) {
            reject(err)
          }
        })
        //  包裹一层,目的同上
        this.onRejectedCallbacks.push((reason) => {
          try {
            let ret = onRejected(reason)
            resolve(ret)
          } catch (err) {
            reject(err)
          }
        })
      }
    })
  }
}

这样,我们就实现了基本的用  then  方法向回调队列里添加函数,并且在未来 promise 对象的状态发生变化时去执行,我们可以写一段测试代码验证一下:

new Promise((resolve, reject) => {
  console.log('inner')
  setTimeout(() => {
    resolve('resolve')
  }, 1000)
}).then((value) => console.log(value))
console.log('outer')
// 输出:
// inner
// outer
// 一秒后输出:resolve

从逻辑上来说,Promise 的主要行为到这里已经基本实现了,但是我们知道, then  方法是有可能返回各种类型的值的,我们之前的测试都只是传递了简单的字符串,也就是说我们还需要对分析标准中提到的解决程序去做进一步的实现。

标准分析中对  [[Resolve]](promise, x)  的情况做了比较清晰的分类,我们接下来要做的就是将文字改写成代码了,这里我们提出一个  resolvePromise  函数来集中存放这部分逻辑,除了需要用到的 promise 和 x 两个参数外,最终用来改变 promise 状态的  resolve  和  reject  函数也一并作为参数传进来:

function resolvePromise(promise, x, resolve, reject) {
  // 当x和promise指向同一实例时,抛出TypeError
  if (x === promise) {
    throw new TypeError("The promise and the value shouldn't be the same");
  }
  // 如果x是一个函数或对象,尝试获取x.then,根据不同情况继续分类
  if ((typeof x === "object" &amp;&amp; x !== null) || typeof x === "function") {
    // 回调只能被执行依次,这里设置一个标记变量
    let hasBeenCalled = false;
    try {
      // 对象上的then accessor可能会直接抛错,赋值需放在try-catch中
      let thenFunction = x.then;
      // 当then是函数时,以当前promise为this执行,向promise的回调队列中继续添加回调
      if (thenFunction &amp;&amp; typeof thenFunction === "function") {
        thenFunction.call(
          x,
          (y) => {
            if (!hasBeenCalled) {
              // 对传入的value(y)继续递归调用resolvePromise
              resolvePromise(promise, y, resolve, reject);
              hasBeenCalled = true;
            }
          },
          (r) => {
            if (!hasBeenCalled) {
              reject(r);
              hasBeenCalled = true;
            }
          }
        );
      } else {
        resolve(x);
      }
    } catch (err) {
      // 如果在获取x.then的过程中抛错,执行reject
      if (hasBeenCalled) {
        reject(err);
        hasBeenCalled = true;
      }
    }
  } else {
    // x不是函数或对象时,直接resolve当前promise即可
    resolve(x);
  }
}

实现了  resolvePromise  后,我们只需要将原代码中直接调用  resolve  的地方替换成使用  resolvePromise  就行了,注意传入  resolve  和  reject  两个参数即可。

目前我们实现的 Promise 已经可以通过测试用例的验证了,不过当前原生的 Promise 提供的功能还不止于此,我们可以进行进一步的扩展。

Promise.resolve

上文用到了  Promise.resolve  和  Promise.reject  两个静态方法,其中  Promise.resolve  方法支持传入 promise,并且返回值的状态跟随传入的 promise 状态。

这个方法内部直接调用了  resolve ,那么我们改写一下这个函数,在开头判断一下 value 类型即可:

let resolve = (value) => {
    if (value instanceof Promise) {
        value.then(resolve, reject)
        return
    }
    // 省略部分代码
}

[promise].catch

这是一个很常用的实例方法,用来捕获异常,实际上就是一个传入了  onRejected  回调的  then  方法:

class Promise {
    catch(callback) {
        return this.then(null, callback)
    }
}

[promise].finally

在 promise 执行完毕后无论结果如何都要进行某些处理时,可以使用这个方法,使用这个方法有几点要注意:

class Promise {
    finally(callback) {
        return this.then((value) => {
            return Promise.resolve(callback()).then(() => value)
        }, (reason) => {
            return Promise.resolve(callback()).then(() => {
                throw reason
            })
        })
    }
}

Promise.all

它返回一个 promise 实例,它在传入  Promise.all  的所有 promise 都被 resolve 时完成,另外传入的参数不包含 promise 时也会直接完成,我们可以给传入的每项添加一个 then 回调,在它被 resolve 时将其加入返回的数组,当数组被填满或者有任意一项被 reject 时返回结果。

Promise.all = function (iterable) {
  // 检测是否为可遍历类型
  if (iterable == null || typeof iterable[Symbol.iterator] !== 'function') {
    return new TypeError(`TypeError: ${typeof iterable} ${iterable} is not iterable`)
  }
  return new Promise((resolve, reject) => {
    // 用于存放结果
    const ret = []
    // 计数器
    let finishedNumber = 0

    function setResultValue(value, index) {
      ret[index] = value
      finishedNumber = finishedNumber + 1
      if (finishedNumber === iterable.length) {
        resolve(ret)
      }
    }

    for (let i = 0, len = iterable.length; i < len; i++) {
      if (iterable[i] &amp;&amp; typeof iterable[i].then === 'function') {
        // 对promise类型在获得结果后将结果添加到数组中
        iterable[i].then((value) => {
          setResultValue(value, i)
        }, reject)
      } else {
        // 非promise类型直接添加到结果
        setResultValue(iterable[i], i)
      }
    }
  })
}

Promise.allSettled

和  Promise.all  不同,这个方法不关心传入的 promise 是否被解决,只要其状态都变更完毕就会被 resolve,实现代码和  Promise.all  唯一的不同就是将 reject 的地方替换为将结果保存在返回数组中:

Promise.allSettled = function (iterable) {
  // 检测是否为可遍历类型
  if (iterable == null || typeof iterable[Symbol.iterator] !== 'function') {
    return new TypeError(`TypeError: ${typeof iterable} ${iterable} is not iterable`)
  }
  return new Promise((resolve, reject) => {
    // 用于存放结果
    const ret = []
    // 计数器
    let finishedNumber = 0

    function setResultValue(value, index) {
      ret[index] = value
      finishedNumber = finishedNumber + 1
      if (finishedNumber === iterable.length) {
        resolve(ret)
      }
    }

    for (let i = 0, len = iterable.length; i < len; i++) {
      if (iterable[i] &amp;&amp; typeof iterable[i].then === 'function') {
        // 对promise类型在获得结果后将结果添加到数组中
        iterable[i].then((value) => {
          setResultValue(value, i)
        }, (reason) => {
          setResultValue(reason, i)
        })
      } else {
        // 非promise类型直接添加到结果
        setResultValue(iterable[i], i)
      }
    }
  })
}

Promise.race

这是我们要实现的最后一个方法了,它的作用是只要传入的某个 promise 被 resolve 或 reject,这个返回的 promise 就会同样被 resolve 和 reject:

Promise.race = function (iterable) {
  // 检测是否为可遍历类型
  if (iterable == null || typeof iterable[Symbol.iterator] !== "function") {
    return new TypeError(
      `TypeError: ${typeof iterable} ${iterable} is not iterable`
    );
  }

  return new Promise((resolve, reject) => {
    for (let i = 0, len = iterable.length; i < len; i++) {
      if (iterable[i] &amp;&amp; typeof iterable[i].then === "function") {
        iterable[i].then(
          (value) => resolve(value),
          (reason) => reject(reason)
        );
      } else {
        resolve(iterable[i], i);
      }
    }
  });
};