逐步手写,实现符合 Promise A+ 规范的 Promise

前言

之前找工作的时候凭感觉做了一个实现 Promise A+ 规范的 Promise的练习,最近在准备新的工作机会,又看到了这个面试题。

我感觉之前的实现有很大优化空间。之前用前次调用结果作为标记来实现 Promise 多次 resolve 和 reject 触发的正确逻辑,感觉有点太麻烦了,通过和 AI 的深入交流,这完全可以用简单的布尔值标记做到。

这篇博客权当是复习吧...

简介 Promise A+ 规范

变量和术语

Promise 表示异步操作的最终结果。

  1. Promise 具有 3 种状态:pending(等待中)、fulfilled(成功执行)、rejected(失败拒绝),初始状态为 pending,切换为 fulfilled 或者 rejected 后就不能再转换。处于非 pending 状态时称为 settled。
ts 复制代码
const testPromise = new Promise((resolve, reject) => {
  // DO SOMETHING
})

像这样子,传入的函数我们称为executorresolvereject会触发 Promise 的状态改变以及数据更新。

value表示成功执行(fulfilled 状态)的 Promise 的结果,reason表示失败拒绝(rejected 状态)的 Promise 的原因,它们可以取 JS 中任何合法的值。

Promise A+ 规范的 Promise 上的方法只有简单的thencatchfinally之类的方法并不包含。

graph A[创建Promise] --> B["执行executor(resolve, reject)"] F{"executor执行结果?"} C -->|settled|G[忽略重复调用] C -->|not settled|E B --> F F -->|"调用resolve(value)"| I{"Promise settled?"} I -->|settled|G I -->|not settled| D["状态: pending → fulfilled
存储value"] F -->|抛出异常| C F -->|"调用reject(reason)"| C{"Promise settled?"} E["状态: pending → rejected
存储reason"] F -->|"当前未执行resolve和reject,没有抛出异常"| H[pending] style A fill:#e1f5ff style B fill:#e1f5ff

then 方法

  1. then方法具有onFulfilledonRejected两个入参,返回一个 Promise(链式调用)。

举个栗子:

ts 复制代码
const temp = testPromise.then(function onFulfilled (value) {
  // DO SOMETHING
}, function onRejected (reason) {
  // DO SOMETHING
})
console.log(temp instanceof Promise) // true
console.log(temp === testPromise) // false
  • Promise 从 pending 状态切换到 fulfilled 或者 rejected 时,执行此前then传入的onFulfilledonRejected。fulfilled 状态的 Promise 会执行then传入的onFulfilled,rejected 状态的 Promise 会执行then传入的onRejected

  • 执行onFulfilledonRejected的结果会被传入新的 Promise tempresolve方法中,如果发生了错误则传入reject中,改变的状态和数据。

  • onFulfilled或者onRejected不是函数时,返回的 Promise 与原 Promise 具有相同的状态和数据(传值穿透)。

用一个流程图总结一下:

graph F["调用promise.then(onFulfilled, onRejected)"] --> G{"当前状态?"} G -->|pending| H["注册回调到队列
等待状态改变"] G -->|fulfilled| I["异步执行onFulfilled(value)"] G -->|rejected| J["异步执行onRejected(reason)"] I --> K{"onFulfilled返回值?"} J --> L{"onRejected返回值?"} K -->|正常返回| M["Promise Resolution Procedure"] L -->|正常返回| M K -->|抛出异常| O["调用新Promise的reject
状态: rejected"] L -->|抛出异常| O H -->|状态变为fulfilled| I H -->|状态变为rejected| J O --> Z[返回新Promise] style F fill:#fff2e1 style G fill:#f0e1ff style M fill:#e8f5e9 style H fill:#fff9c4

Promise Resolution Procedure

  1. resolve被触发时发生什么事了?此时 Promise 的状态仍未真正变化,会进入一段处理程序,规范称之为 Promise Resolution Procedure,主要逻辑是如果传入的是非 thenable 对象或者基本类型则直接修改 Promise 的状态和数据,是 thenable 就执行下面 thenable 相关逻辑。
  • 此外,不支持我返回我自己,onFulfilled或者onRejected返回该then返回的 Promise 时,抛出TypeError错误,例如:
ts 复制代码
const temp = testPromise.then(function onFulfilled (value) {
  return temp
})

处理 thenable 对象

  1. thenable 的对象是具有then方法的对象或者函数。then方法接受两个回调函数onResolvePromiseonRejectPromise,类似于这里的 Promise 的then。thenable 实际上包括实现了 Promise A+ 规范的 Promise,例如 ES6 原生的 Promise。举个 thenable 对象的栗子:
ts 复制代码
const thenable = {
  then: function (onResolvePromise, onRejectPromise) {
    onResolvePromise('miao~~')
  }
}
  • 如果触发了onFulfilled,返回了一个 thenable。如果是该 Promise 的实例,不是当前 Promise,则传入当前 Promise 的resolvereject,调用then方法。

  • 兼容其他 thenable:调用then方法,传入当前 Promise 的resolvereject,像该 Promise 实例一样解析。

  • 允许其他 thenable 对象乱写 ,这里需要处理 thenable 对象重复触发onResolvePromise或/和onRejectPromise的情况,这两个回调函数最多只能改变 1 次 Promise 的状态。

  1. 其他详细见 Promise A+ 规范。

这里再用个流程图总结一下

graph M["Promise Resolution Procedure"] M --> P{返回值是thenable?} P -->|是| Q{是否返回自身?} Q -->|是| R[抛出TypeError] Q -->|否| S["调用thenable.then(resolvePromise, rejectPromise)"] P -->|否| N S --> T{thenable行为?} T -->|"调用resolvePromise(x)"| U{Promise settled?} T -->|"调用rejectPromise(reason)"| V{Promise settled?} T -->|抛出异常| O[调用新Promise的reject
状态: rejected] U -->|not settled| W["状态: fulfilled
value = x"] V -->|not settled| X["状态: rejected
reason = reason"] U -->|settled| Y[忽略重复调用] V -->|settled| Y N[调用新Promise的resolve
状态: fulfilled] --> Z[返回新Promise] O --> Z W --> Z X --> Z R --> AA["返回rejected Promise
reason = TypeError"] style M fill:#e8f5e9

前期准备

先定义好类型和一个发起微任务的辅助函数。

ts 复制代码
enum PromiseState {
  fulfilled = 'fulfilled',
  pending = 'pending',
  rejected = 'rejected'
}

type Executor<T> = (
  resolve: (value: T | PromiseLike<T>) => void,
  reject: (reason?: any) => void
) => void

const scheduleMicrotask = (callback: () => void) => {
  if (typeof queueMicrotask === 'function') {
    queueMicrotask(callback)
  } else if (typeof process !== 'undefined' && process.nextTick) {
    process.nextTick(callback)
  } else {
    Promise.resolve().then(callback)
  }
}

简单地写一个 Promise

ts 复制代码
class ShikaPromise<T = any> {
  private state: PromiseState = PromiseState.pending
  private value: T | undefined
  private reason: any
  constructor(executor: Executor<T>) {
    try {
      executor(
        (value) => this.resolve(value),
        (reason) => this.reject(reason)
      )
    } catch (error) {
      this.reject(error)
    }
  }
  private resolve(value: T): void {
    // 不支持 resolve 自己
    if (value === this) {
      this.reject(new TypeError('Cannot resolve promise with itself'))
      return
    }
    
    scheduleMicrotask(() => {
      this.state = PromiseState.fulfilled
      this.value = value
    })
  }
  private reject(reason: any): void {
    scheduleMicrotask(() => {
      this.state = PromiseState.rejected
      this.reason = reason
    })
  }
  then<TResult1 = T, TResult2 = never>(
    onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
    onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
  ) {
    // TODO
  }
}

下面就来写then方法实现异步的链式调用。

then 方法

then返回一个 Promise,虽然 Promise A+ 规范没有说明需要返回的 Promise 不能和原有的是同一个,但是考虑到后续链式调用也会涉及到 Promise 状态的改变,所以这里就返回一个新的 Promise。

fulfilled 和 rejected 状态

假设const promise2 = promise1.then(onFulfilled, onRejected),调用promise1.then时创建一个新的 Promise promise2返回出去。用过 ES6 的Promise很好理解,如果原有promise1是 fulfilled 的,则在新的promise2executor中的resolve传入onFulfilled的结果,如果promise1处于失败状态,rejected 了,则在promise2resolve中传入onRejected的结果。

举个栗子:

ts 复制代码
const promiseTmp1 = Promise.resolve('ok').then(value => value, reason => reason)
// 此时 promiseTmp1.value 是 'ok'
const promiseTmp2 = Promise.resolve('error').then(value => value, reason => reason)
// 此时 promiseTmp2.value 是 'error'

下面编写 fulfilled 和 rejected 状态的处理逻辑。

ts 复制代码
// ...

class ShikaPromise {
  // ...
  then<TResult1 = T, TResult2 = never>(
    onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
    onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
  ): ShikaPromise<TResult1 | TResult2> {
    return new ShikaPromise<TResult1 | TResult2>((resolve, reject) => {
      const handleCallback = (isFulfilled: boolean) => {
        scheduleMicrotask(() => {
          const callback = isFulfilled ? onFulfilled : onRejected
          const data = isFulfilled ? this.value : this.reason
          // 传值穿透
          if (typeof callback !== 'function') {
            if (isFulfilled) {
              resolve(data as TResult1)
            } else {
              reject(data)
            }
            return
          }

          try {
            const result = callback(data)
            resolve(result)
          } catch (error) {
            reject(error)
          }
        })
      }

      switch (this.state) {
        case PromiseState.fulfilled:
          handleCallback(true)
          break
        case PromiseState.rejected:
          handleCallback(false)
          break
        default:
          // TODO
      }
    })
  }
}

pending 状态

promise1在等待的时候,可以在promise1上新建两个属性fulfilledHandlersrejectedHandlers缓存给promise2触发resolvereject的回调函数。promise2处于 pending 状态,promise1切换状态后触发这些回调函数,用来改变promise2的状态。

ts 复制代码
// ...
class ShikaPromise {
  // ...
  // 记录等待 fulfilled 或者 rejected 后执行的回调函数
  private fulfilledHandlers: Array<() => void> = []
  private rejectedHandlers: Array<() => void> = []
  // ...
  private resolve(value: T): void {
    scheduleMicrotask(() => {
      this.state = PromiseState.fulfilled
      this.value = value

      const handlers = this.fulfilledHandlers.splice(0)
      handlers.forEach((h) => h())
    })
  }
  private reject(reason: any): void {
    scheduleMicrotask(() => {
      this.state = PromiseState.rejected
      this.reason = reason

      const handlers = this.rejectedHandlers.splice(0)
      handlers.forEach((h) => h())
    })
  }
  // ...
  then (onFulfilled?: ThenCallback, onRejected?: ThenCallback) {
    return new ShikaPromise<TResult1 | TResult2>((resolve, reject) => {
      // ...
      switch (this.state) {
        // ...
        default:
          this.fulfilledHandlers.push(() => handleCallback(true))
          this.rejectedHandlers.push(() => handleCallback(false))
      }
    })
  }
}

防止多次触发

我们通过添加标记isResolved记录是否已经触发resolve。当重复触发resolvereject时,遇到isResolvedtrue就返回。

ts 复制代码
// ...
class ShikaPromise<T = any> {
  // ...
  private isResolved = false
  // ...

  private resolve(value: T | PromiseLike<T>): void {
    if (this.isResolved) return
    
    if (value === this) {
      this.reject(new TypeError('Cannot resolve promise with itself'))
      return
    }
    
    // TODO: thenable 处理
    this.fulfill(value as T)
  }

  private fulfill(value: T): void {
    if (this.isResolved) return
    this.isResolved = true
    
    scheduleMicrotask(() => {
      this.state = PromiseState.fulfilled
      this.value = value

      const handlers = this.fulfilledHandlers.splice(0)
      handlers.forEach((h) => h())
    })
  }

  private reject(reason: any): void {
    if (this.isResolved) return
    this.isResolved = true
    // ...
  }
  // ...
}

解析 thenable 对象

如果遇到 thenable 对象,等待其进入 fulfilled 或者 rejected 状态,同样的,thenable 对象也需要防止重复进入 fulfilled 和 rejected 状态。

ts 复制代码
class ShikaPromise<T = any> {
  // ...

  private resolve(value: T | PromiseLike<T>): void {
    // ...
    const thenable = this.getThenable(value)
    if (thenable) {
      this.resolveThenable(thenable)
    } else {
      this.fulfill(value as T)
    }
  }
  private getThenable(value: any): { then: Function; target: any } | null {
    if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
      try {
        // 在规范中有 Let then be x.then 的描述,测试用例中 value.then 只能被取一次
        const then = value.then
        if (typeof then === 'function') {
          return { then, target: value }
        }
      } catch (error) {
        this.reject(error)
      }
    }
    return null
  }

  private resolveThenable(thenable: { then: Function; target: any }): void {
    let called = false

    try {
      thenable.then.call(
        thenable.target,
        (value: any) => {
          if (called) return
          called = true
          this.resolvevaluey)
        },
        (reason: any) => {
          if (called) return
          called = true
          this.reject(reason)
        }
      )
    } catch (error) {
      if (!called) this.reject(error)
    }
  }
}

其他方法

JS 的 Promise

下面就来实现一下 JS 的 Promse 的catchfinallycatch就是then方法只提供第二个参数。finally方法回调函数不接收任何参数,返回一个状态和数据与原来相同的 Promise。

ts 复制代码
class ShikaPromise {
  catch<TResult = never>(
    onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
  ): ShikaPromise<T | TResult> {
    return this.then(null, onRejected)
  }

  finally(onFinally?: (() => void) | null | undefined): ShikaPromise<T> {
    return this.then(
      (value) => {
        onFinally?.()
        return value
      },
      (reason) => {
        onFinally?.()
        throw reason
      }
    )
  }
}

还有Promise.resolvePromise.reject两个静态方法:

ts 复制代码
class ShikaPromise {
  static resolve<T>(value: T | PromiseLike<T>): ShikaPromise<T> {
    return value instanceof ShikaPromise ? value : new ShikaPromise((resolve) => resolve(value))
  }
  static reject<T = never>(reason?: any): ShikaPromise<T> {
    return new ShikaPromise((_, reject) => reject(reason))
  }
}

如果 Promise 可以停止

如果想要 Promise 后面的thencatchfinally)都不会触发,这里只需要返回一个 pending 状态的 Promise。这里实现一个时链式调用停止的cancel方法和返回 pending 的 Promise 的wait方法:

ts 复制代码
class ShikaPromise {
  static wait(): ShikaPromise<never> {
    return new ShikaPromise(() => {})
  }
  cancel(): ShikaPromise<never> {
    return new ShikaPromise(() => {})
  }
}

Promise A+ 测试

下载 promises-aplus-tests 包:

cmd 复制代码
npm i promises-aplus-tests

要求 Promise 所在文件采用 commonjs 方式导出。还需要在 Promise 上实现静态方法:

ts 复制代码
class ShikaPromise {
  static deferred<T>() {
    let resolve!: (value: T | PromiseLike<T>) => void
    let reject!: (reason?: any) => void

    const promise = new ShikaPromise<T>((res, rej) => {
      resolve = res
      reject = rej
    })

    return { promise, resolve, reject }
  }
}

promises-aplus-tests Promise 的所在文件即可运行,如果你在用 TS,文件为编译后的文件,例如:

cmd 复制代码
promises-aplus-tests dist/文件名.js

Promise A+ 的测试用例覆盖面非常全,调试时烦死了x,通过了所有 817 条用例,就说明你的 Promise 实现了 Promise A+ 标准了。

我把 TS 编译和运行测试用例在 package.json 组装成一条命令:

json 复制代码
{
  // ...
  "scripts": {
    // ...
    "test": "tsc && promises-aplus-tests dist/文件名.js",
  },
  // ...
}

这里 tsc 会默认编译 tsconfig.json 设置的根目录(这里是 ./src),然后放到输出目录中(这里是 ./dist)。

最终实现

ts 复制代码
enum PromiseState {
  fulfilled = 'fulfilled',
  pending = 'pending',
  rejected = 'rejected'
}

type Executor<T> = (
  resolve: (value: T | PromiseLike<T>) => void,
  reject: (reason?: any) => void
) => void

const scheduleMicrotask = (callback: () => void) => {
  if (typeof queueMicrotask === 'function') {
    queueMicrotask(callback)
  } else if (typeof process !== 'undefined' && process.nextTick) {
    process.nextTick(callback)
  } else {
    Promise.resolve().then(callback)
  }
}

class ShikaPromise<T = any> {
  private state: PromiseState = PromiseState.pending
  private value: T | undefined
  private reason: any
  private fulfilledHandlers: Array<() => void> = []
  private rejectedHandlers: Array<() => void> = []
  private isResolved = false

  constructor(executor: Executor<T>) {
    try {
      executor(
        (value) => this.resolve(value),
        (reason) => this.reject(reason)
      )
    } catch (error) {
      this.reject(error)
    }
  }

  private resolve(value: T | PromiseLike<T>): void {
    if (this.isResolved) return

    if (value === this) {
      this.reject(new TypeError('Cannot resolve promise with itself'))
      return
    }

    const thenable = this.getThenable(value)
    if (thenable) {
      this.resolveThenable(thenable)
    } else {
      this.fulfill(value as T)
    }
  }

  private fulfill(value: T): void {
    if (this.isResolved) return
    this.isResolved = true

    scheduleMicrotask(() => {
      this.state = PromiseState.fulfilled
      this.value = value

      const handlers = this.fulfilledHandlers.splice(0)
      handlers.forEach((h) => h())
    })
  }

  private reject(reason: any): void {
    if (this.isResolved) return
    this.isResolved = true

    scheduleMicrotask(() => {
      this.state = PromiseState.rejected
      this.reason = reason

      const handlers = this.rejectedHandlers.splice(0)
      handlers.forEach((h) => h())
    })
  }

  private getThenable(value: any): { then: Function; target: any } | null {
    if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
      try {
        const then = value.then
        if (typeof then === 'function') {
          return { then, target: value }
        }
      } catch (error) {
        this.reject(error)
      }
    }
    return null
  }

  private resolveThenable(thenable: { then: Function; target: any }): void {
    let called = false

    try {
      thenable.then.call(
        thenable.target,
        (value: any) => {
          if (called) return
          called = true
          this.resolve(value)
        },
        (reason: any) => {
          if (called) return
          called = true
          this.reject(reason)
        }
      )
    } catch (error) {
      if (!called) this.reject(error)
    }
  }

  then<TResult1 = T, TResult2 = never>(
    onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
    onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
  ): ShikaPromise<TResult1 | TResult2> {
    return new ShikaPromise<TResult1 | TResult2>((resolve, reject) => {
      const handleCallback = (isFulfilled: boolean) => {
        scheduleMicrotask(() => {
          const callback = isFulfilled ? onFulfilled : onRejected
          const data = isFulfilled ? this.value : this.reason

          if (typeof callback !== 'function') {
            if (isFulfilled) {
              resolve(data as TResult1)
            } else {
              reject(data)
            }
            return
          }

          try {
            const result = callback(data)
            resolve(result)
          } catch (error) {
            reject(error)
          }
        })
      }

      switch (this.state) {
        case PromiseState.fulfilled:
          handleCallback(true)
          break
        case PromiseState.rejected:
          handleCallback(false)
          break
        default:
          this.fulfilledHandlers.push(() => handleCallback(true))
          this.rejectedHandlers.push(() => handleCallback(false))
      }
    })
  }

  catch<TResult = never>(
    onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
  ): ShikaPromise<T | TResult> {
    return this.then(null, onRejected)
  }

  finally(onFinally?: (() => void) | null | undefined): ShikaPromise<T> {
    return this.then(
      (value) => {
        onFinally?.()
        return value
      },
      (reason) => {
        onFinally?.()
        throw reason
      }
    )
  }

  static resolve<T>(value: T | PromiseLike<T>): ShikaPromise<T> {
    return value instanceof ShikaPromise ? value : new ShikaPromise((resolve) => resolve(value))
  }

  static reject<T = never>(reason?: any): ShikaPromise<T> {
    return new ShikaPromise((_, reject) => reject(reason))
  }

  static wait(): ShikaPromise<never> {
    return new ShikaPromise(() => {})
  }

  cancel(): ShikaPromise<never> {
    return new ShikaPromise(() => {})
  }

  static deferred<T>() {
    let resolve!: (value: T | PromiseLike<T>) => void
    let reject!: (reason?: any) => void

    const promise = new ShikaPromise<T>((res, rej) => {
      resolve = res
      reject = rej
    })

    return { promise, resolve, reject }
  }
}

module.exports = ShikaPromise

结尾

这里实现了一个 Promise A+ 规范的 Promise,重新理解 Promise A+ 规范也修复了我以前对此的认识不足之处。

大家的阅读是我发帖的动力,本文首发于我的博客:deerblog.gu-nami.com/,欢迎大家来玩, 转载请注明出处。

相关推荐
一千柯橘14 分钟前
Electron - IPC 解决主进程和渲染进程之间的通信
前端
申阳15 分钟前
Day 16:02. 基于 Tauri 2.0 开发后台管理系统-项目初始化配置
前端·后端·程序员
老前端的功夫17 分钟前
HTTP 协议演进深度解析:从 1.0 到 2.0 的性能革命
前端·网络·网络协议·http·前端框架
拉不动的猪21 分钟前
前端三大权限场景全解析:设计、实现、存储与企业级实践
前端·javascript·面试
封奚泽优32 分钟前
下降算法(Python实现)
开发语言·python·算法
im_AMBER39 分钟前
算法笔记 16 二分搜索算法
c++·笔记·学习·算法
高洁0141 分钟前
【无标具身智能-多任务与元学习】
神经网络·算法·aigc·transformer·知识图谱
wordbaby1 小时前
Flutter Form Builder 完全指南:告别 Controller 地狱
前端·flutter
A***07171 小时前
React数据可视化应用
前端·react.js·信息可视化