实现 Promise A+ 规范的 Promise
前言
之前找工作的时候凭感觉做了一个实现 Promise A+ 规范的 Promise的练习,最近在准备新的工作机会,又看到了这个面试题。
我感觉之前的实现有很大优化空间。之前用前次调用结果作为标记来实现 Promise 多次 resolve 和 reject 触发的正确逻辑,感觉有点太麻烦了,通过和 AI 的深入交流,发现完全可以用简单的布尔值标记做到。
这篇博客是前文的优化版,包含了前文已有的 Promise A+ 的一些介绍,权当是复习吧…
前文看这里:https://deer.shika-blog.xyz/web/article/108 (各种错误有点多)
简介 Promise A+ 规范
变量和术语
Promise 表示异步操作的最终结果。
- Promise 具有 3 种状态:pending(等待中)、fulfilled(成功执行)、rejected(失败拒绝),初始状态为 pending,切换为 fulfilled 或者 rejected 后就不能再转换。处于非 pending 状态时称为 settled。
ts- const testPromise = new Promise((resolve, reject) => {
- // DO SOMETHING
- })
像这样子,传入的函数我们称为executor,resolve和reject会触发 Promise 的状态改变以及数据更新。
value表示成功执行(fulfilled 状态)的 Promise 的结果,reason表示失败拒绝(rejected 状态)的 Promise 的原因,它们可以取 JS 中任何合法的值。
Promise A+ 规范的 Promise 上的方法只有简单的
then,catch、finally之类的方法并不包含。
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 方法
then方法具有onFulfilled和onRejected两个入参,返回一个 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传入的onFulfilled或onRejected。fulfilled 状态的 Promise 会执行then传入的onFulfilled,rejected 状态的 Promise 会执行then传入的onRejected。 -
执行
onFulfilled或onRejected的结果会被传入新的 Promisetemp的resolve方法中,如果发生了错误则传入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
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 对象
- thenable 的对象是具有
then方法的对象或者函数。then方法接受两个回调函数onResolvePromise和onRejectPromise,类似于这里的 Promise 的then。thenable 实际上包括实现了 Promise A+ 规范的 Promise,例如 ES6 原生的 Promise。举个 thenable 对象的栗子:
ts- const thenable = {
- then: function (onResolvePromise, onRejectPromise) {
- onResolvePromise('miao~~')
- }
- }
-
如果触发了
onFulfilled,返回了一个 thenable。如果是该 Promise 的实例,不是当前 Promise,则传入当前 Promise 的resolve和reject,调用then方法。 -
兼容其他 thenable:调用
then方法,传入当前 Promise 的resolve和reject,像该 Promise 实例一样解析。 -
允许其他 thenable 对象乱写,这里需要处理 thenable 对象重复触发onResolvePromise或/和onRejectPromise的情况,这两个回调函数最多只能改变 1 次 Promise 的状态。
- 其他详细见 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 的,则在新的promise2的executor中的resolve传入onFulfilled的结果,如果promise1处于失败状态,rejected 了,则在promise2的resolve中传入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上新建两个属性fulfilledHandlers、rejectedHandlers缓存给promise2触发resolve和reject的回调函数。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。当重复触发resolve和reject时,遇到isResolved为true就返回。
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 的catch和finally。catch就是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.resolve和Promise.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 后面的then(catch、finally)都不会触发,这里只需要返回一个 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+ 规范也修复了我以前对此的认识不足之处。
