Published on

TypeScript实现Promise

Authors
  • avatar
    Name
    Et cetera
    Twitter
enum PromiseStatus {
  Pending = 'pending',
  Fulfilled = 'fulfilled',
  Rejected = 'rejected',
}

interface IExecutor<T> {
  (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void): void
}

interface Thenable<T> {
  then<TResult1 = T, TResult2 = never>(
    onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
    onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
  ): Promise<TResult1 | TResult2>
}

class Promise<T> implements Thenable<T> {
  private status: PromiseStatus = PromiseStatus.Pending
  private value: T | undefined
  private reason: any | undefined
  private onFulfilledCallbacks: ((value: T) => void)[] = []
  private onRejectedCallbacks: ((reason: any) => void)[] = []

  constructor(executor: IExecutor<T>) {
    try {
      executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (error) {
      this.reject(error)
    }
  }

  private resolve(value?: T | PromiseLike<T>): void {
    if (this.status === PromiseStatus.Pending) {
      if (value instanceof Promise) {
        value.then(this.resolve.bind(this), this.reject.bind(this))
      } else {
        this.status = PromiseStatus.Fulfilled
        this.value = value
        this.onFulfilledCallbacks.forEach((callback) => callback(this.value as T))
      }
    }
  }

  private reject(reason?: any): void {
    if (this.status === PromiseStatus.Pending) {
      this.status = PromiseStatus.Rejected
      this.reason = reason
      this.onRejectedCallbacks.forEach((callback) => callback(this.reason))
    }
  }

  public then<TResult = T, TErrResult = never>(
    onFulfilled?: ((value: T) => TResult | PromiseLike<TResult>) | null,
    onRejected?: ((reason: any) => TErrResult | PromiseLike<TErrResult>) | null
  ): Promise<TResult | TErrResult> {
    const promise = new Promise<TResult | TErrResult>((resolve, reject) => {
      if (this.status === PromiseStatus.Fulfilled) {
        try {
          const result = onFulfilled
            ? onFulfilled(this.value as T)
            : (this.value as unknown as TResult)
          promiseResolutionProcedure(promise, result, resolve, reject)
        } catch (error) {
          reject(error)
        }
      } else if (this.status === PromiseStatus.Rejected) {
        try {
          const result = onRejected
            ? onRejected(this.reason)
            : (this.reason as unknown as TErrResult)
          promiseResolutionProcedure(promise, result, resolve, reject)
        } catch (error) {
          reject(error)
        }
      } else {
        this.onFulfilledCallbacks.push((value: T) => {
          try {
            const result = onFulfilled
              ? onFulfilled(value)
              : (value as unknown as TResult)
            promiseResolutionProcedure(promise, result, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
        this.onRejectedCallbacks.push((reason: any) => {
          try {
            const result = onRejected
              ? onRejected(reason)
              : (reason as unknown as TErrResult)
            promiseResolutionProcedure(promise, result, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      }
    })
    return promise
  }

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

  public finally(onFinally?: (() => void) | null): Promise<T> {
    return this.then<T>(
      (value) => Promise.resolve(onFinally && onFinally()).then(() => value),
      (reason) =>
        Promise.resolve(onFinally && onFinally()).then(() => {
          throw reason
        })
    )
  }

  public static race<T>(promises: Iterable<T | PromiseLike<T>>): Promise<T> {
    return new Promise((resolve, reject) => {
      for (const promise of promises) {
        Promise.resolve(promise).then(resolve, reject)
      }
    })
  }

  public static all<T>(promises: Iterable<T | PromiseLike<T>>): Promise<T[]> {
    const result: T[] = []
    const promiseCount = Array.isArray(promises)
      ? promises.length
      : Object.keys(promises).length
    const isIterable = typeof promises[Symbol.iterator] === 'function'

    if (promiseCount === 0) {
      return Promise.resolve(result)
    }

    let resolveCount = 0
    return new Promise((resolve, reject) => {
      function handleResolved(index: number, value: T) {
        result[index] = value
        resolveCount++
        if (resolveCount === promiseCount) {
          resolve(result)
        }
      }

      function handleRejected(reason: any) {
        reject(reason)
      }

      let index = 0
      for (const promise of isIterable
        ? (promises as Iterable<T | PromiseLike<T>>)
        : Object.values(promises)) {
        Promise.resolve(promise).then((value: T) => {
          handleResolved(index, value)
        }, handleRejected)

        index++
      }
    })
  }
}

function promiseResolutionProcedure<T>(
  promise: Promise<T>,
  x: unknown,
  resolve: (value?: T | PromiseLike<T>) => void,
  reject: (reason?: any) => void
): void {
  if (promise === x) {
    reject(new TypeError('Chaining cycle detected for promise'))
  } else if (x instanceof Promise) {
    x.then(resolve, reject)
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = (x as { then: Function }).then
      if (typeof then === 'function') {
        let called = false
        try {
          then.call(
            x,
            (y: unknown) => {
              if (!called) {
                called = true
                promiseResolutionProcedure(promise, y, resolve, reject)
              }
            },
            (r: any) => {
              if (!called) {
                called = true
                reject(r)
              }
            }
          )
        } catch (error) {
          if (!called) {
            called = true
            reject(error)
          }
        }
      } else {
        resolve(x as T)
      }
    } catch (error) {
      reject(error)
    }
  } else {
    resolve(x as T)
  }
}