likes
comments
collection
share

基于 Promise A+ 规范使用Typescript实现Promise

作者站长头像
站长
· 阅读数 14

因为在之前已经写过Promise基础使用的文章了,具体可参见: 浅显易懂Promise(一)

所以直接进入正题,使用Typescript来实现一个Promise,本文涉及到的代码已经上传到github,顺便安利一下tiny-ts-tools,这个项目会使用Typescript来实现一些经典的前端库,相关库核心逻辑的最简实现,以及分步骤实现的文章。接下来会实现mini-nest依赖注入、vuex4、axios欢迎star! 地址:Tiny-Blog

一. 实现同步的promise

首先先来写一个🌰,一个同步执行的Promise,执行完Promise函数中的内容后,执行其.then中的逻辑:

 let p = new Promise((resolve, reject) => {
   resolve('小黄瓜')
 }).then((val)=>{
   console.log(val, 'val'); // 小黄瓜
 }, () => {})

根据上面例子得执行结果,我们可以得出一个很简单的结论:Promise是一个构造函数(类),接受一个匿名函数,该函数接受两个参数,分别是resolve函数和reject函数,分别代表变更Promise状态为"成功"和"失败"。 当函数执行完毕后,会调用Promise的实例方法then,将执行结果传递给then方法,然后执行then方法中的逻辑。

那么先实现一个Promise类:

 export default class TinyPromise<T = any> {
   // 当前promise状态
   private _status = 'pedding'
   // 当前执行状态接收的值,用于传递给then函数
   private _value: any
 ​
   constructor(executor: ExecutorType<T>) {
     // 使用try进行错误捕获,如果执行失败直接调用_reject返回错误
     try {
       // 调用用户传递的函数,将_resolve和_reject作为参数传递
       executor(this._resolve.bind(this), this._reject.bind(this))
     } catch (error) {
       this._reject(error)
     }
   }
 ​
   private _resolve: Resolve<T> = (resolveValue) {
     // 变更状态为成功
     this._status = 'fulfilled'
     // 保存执行结果
     this._value = resolveValue
   }
   private _reject: Reject = (error) {
     // 变更状态为失败
     this._status = 'rejected'
     this._value = error
   }
 }

在这个Promise类中首先定义了一个泛型T,在用户传递的executor函数将其约束为ExecutorType类型,它的实现是这样的:

 type ExecutorType<T> = (resolve: Resolve<T>, reject: Reject) => void

因为我们是直接将_resolve_reject这两个实例方法传入到executor函数中,所以ResolveReject类型当然也可以对_resolve_reject这两个实例方法进行约束:

 // _resolve
 type Resolve<T = any> = (resolveValue: T | PromiseLike<T>) => void
 // _reject
 type Reject = (rejectValue: any) => void

为什么_resolve的参数被约束为了T | PromiseLike<T>? PromiseLike是什么?PromiseLike其实是ts内置的一个类型,它的实现是这样的:

 interface PromiseLike<T> {
     /**
      * Attaches callbacks for the resolution and/or rejection of the Promise.
      * @param onfulfilled The callback to execute when the Promise is resolved.
      * @param onrejected The callback to execute when the Promise is rejected.
      * @returns A Promise for the completion of which ever callback is executed.
      */
     then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
 }

其实就是定义了一个包含有then方法,接受onfulfilledonrejected函数的接口,可以理解为是一个"Promise"类型,相当于使用它来代表Promise进行约束。 我们的_resolve方法未来可以接受一个Promise作为参数,所以将参数的约束定义为T | PromiseLike<T>

然后将_resolve_reject函数的参数保存到_value中,在未来调用then方法的时候使用。

这里还有一个优化的点,我们可以将fulfilledrejectedpedding三种状态定义为枚举值,便于维护:

 enum PROMISESTATUS {
   PENDING = 'pending',
   FULFILLED = 'fulfilled',
   REJECTED = 'rejected'
 }

相应的,我们的代码也可以改造成这样:

 export default class TinyPromise<T = any> {
   // 初始状态为pedding
   private _status = PROMISESTATUS.PENDING
   private _value: any
 ​
   constructor(executor: ExecutorType<T>) {
     try {
       executor(this._resolve.bind(this), this._reject.bind(this))
     } catch (error) {
       this._reject(error)
     }
   }
 ​
   private _resolve: Resolve<T> = (resolveValue) {
     this._status = PROMISESTATUS.FULFILLED
     this._value = resolveValue
   }
   private _reject: Reject = (error) {
     this._status = PROMISESTATUS.REJECTED
     this._value = error
   }
 }

接下来就可以实现then方法了,其实then方法的作用很简单,包括以后实现的更复杂的实现。它的核心作用只有一个,就是作为executor函数执行后的回调函数then方法接收两个函数作为参数,其中第一个函数代表执行成功状态的回调,第二个函数代表执行失败状态的回调。

 // 暂时将resolveInThen与rejectinThen定义为any
 public then(resolveInThen: any, rejectinThen: any) {
   const {
     _value,
     _status
   } = this
 ​
   if(_status === PROMISESTATUS.FULFILLED) {
     resolveInThen(_value)
   }
 ​
   if(_status === PROMISESTATUS.REJECTED) {
     rejectinThen(_value)
   }
 }

这就实现了一个最基本的同步执行的Promise来试一下上面的🌰:

 let p = new TinyPromise((resolve, reject) => {
   resolve('小黄瓜')
 }).then((val)=>{
   console.log(val, 'val'); // 小黄瓜
 }, () => {})

二. 实现异步的Promise & 链式调用

其实上面实现的同步功能没有什么作用,因为Promise本来就是处理异步,如果只能执行同步代码,那还有什么用😂。

 let p = new TinyPromise((resolve) => {
   setTimeout(() => {
     resolve('小黄瓜')
   }, 1000);
 }).then((val)=>{
   console.log(val, '第一个then');
   return '小南瓜'
 }, () => {}).then((val)=>{
   console.log(val, '第二个then');
 }, () => {})

上面这个🌰我们想要实现的效果是1秒钟后执行resolve函数,传入'小黄瓜',然后执行第一个then函数,将执行器的参数进行传递,最后执行第二个then函数,把第一个then函数的返回值当作参数传入。

如果按照上一节实现的功能,肯定是满足不了目前的需求,首先按照js的执行机制,我们在执行器内定义了一个定时器,当js代码遇到异步任务时,会暂时挂起,先执行同步代码,所以在这个例子中,会先执行then函数的代码,而此时Promise的状态依然是pedding,因为执行器定义了一个定时器,所以在1秒钟后Promise的状态才被变更为fulfilledthen函数中的代码无法被正确执行。

其实解决的思路也很简单,我们如果能让then函数内接收的函数执行晚于执行器内的异步代码就可以了!如果执行then方法时,Promise的状态还是处于pedding状态,那么此时在执行器内的逻辑就肯定是异步代码!然后就将then方法传入的函数先收集,再执行!先将回调函数收集起来,等异步操作执行完毕后,再执行回调函数。

那么链式调用怎么实现呢,其实直接返回一个Promise就可以了,返回一个Promise实例,当然可以在后面继续调用then方法。

TinyPromise进行改造:

 ​
   export default class TinyPromise<T = any> {
   private _status = PROMISESTATUS.PENDING
   private _value: any
   // 成功回调函数队列
   private _fulfilledQueues: Function[] = []
   // 失败回调函数队列
   private _rejectedQueues: Function[] = []
 ​
   constructor(executor: ExecutorType<T>) {
     try {
       executor(this._resolve.bind(this), this._reject.bind(this))
     } catch (error) {
       this._reject(error)
     }
   }
 ​
   _resolve(resolveValue: any) {
     if(this._status !== PROMISESTATUS.PENDING) return
     const runFulfilled = (value: any) => {
       let cb;
       // 取出每个函数执行
       while(cb = this._fulfilledQueues.shift()) {
         cb(value)
       }
     }
     // 更改状态 保存参数 执行回调函数
     this._value = resolveValue
     this._status = PROMISESTATUS.FULFILLED
     runFulfilled(resolveValue)
   }
   _reject(error: any) {
     this._status = PROMISESTATUS.REJECTED
     this._value = error
     let cb;
     while (cb = this._rejectedQueues.shift()) {
       cb(error)
     }
   }
 ​
   then(resolveInThen: any, rejectinThen: any) {
     const {
       _value,
       _status
     } = this
     // 返回Promise实例
     return new TinyPromise((resolveNext, rejectNext) => {
       // 封装执行成功的操作
       const fulfilled = (val: any) => {
         let res = resolveInThen(val);
         // 向后传递执行结果
         resolveNext(res)
       }
       // 封装执行失败的操作
       const rejected = (error: any) => {
         let res = rejectinThen(error);
         // 向后传递执行结果
         rejectNext(res)
       }
       // 判断当前Promise状态:pedding:保存 fulfilled/rejected:直接执行
       switch(_status) {
         case PROMISESTATUS.PENDING:
           this._fulfilledQueues.push(fulfilled)
           this._rejectedQueues.push(rejected)
         break
         case PROMISESTATUS.FULFILLED:
           fulfilled(_value)
         break
         case PROMISESTATUS.REJECTED:
           rejected(_value)
         break
       }
     })
   }
 }

我们定义了两个队列:_fulfilledQueues_rejectedQueues,分别用于保存成功和失败的回调函数,在异步操作执行完毕后,取出两个队列中的函数进行执行。

执行上文中的🌰:

 小黄瓜 第一个异步函数
 小南瓜 第二个异步函数

三. 实现多级多异步

接下来实现在then方法中再次定义异步方法,此时应该将后续的then作为异步then函数的回调函数来调用。

 let p = new TinyPromise((res) => {
   setTimeout(() => {
     res('第一个异步Promise')
   }, 1000)
 }).then((val: any)=>{
   console.log(val, '第一个then');
   // 再次定义异步任务
   return new TinyPromise((res, rej) => {
     setTimeout(() => {
       res('第二个异步Promise')
     }, 1000)
   })
 }, () => {}).then((val: any)=>{
   // 此时的then应当作为上一个异步的then
   console.log(val, '第二个then');
 }, () => {})

在增加了多异步的逻辑之后要在then方法中定义执行函数时,对各种情况进行判断:

  • 如果参数为非函数,则直接向后传递
  • 如果参数为函数,执行函数,并保存执行结果向后传递
  • 如果参数为Promise,调用then

接下来就实现改造后的then方法:

 then(resolveInThen: any, rejectinThen: any) {
     const {
       _value,
       _status
     } = this
     return new TinyPromise((resolveNext, rejectNext) => {
 ​
       const fulfilled = (val: any) => {
         try {
           // 非函数,直接向后传递
           if (!isFunction(resolveInThen)) {
             resolveNext(val)
           } else {
             // 执行函数
             let res = resolveInThen(val);
             // Promise调用then
             if (isPromise(res)) {
               res.then(resolveNext, rejectNext)
             } else {
               // 返回结果
               resolveNext(res)
             }
           }
         } catch (error) {
           rejectNext(error)
         }
       }
       // 失败状态的处理逻辑类似
       const rejected = (error: any) => {
         try {
           if (!isFunction(rejectinThen)) {
             rejectNext(error)
           } else {
             let res = rejectinThen(error);
             if (isPromise(res)) {
               res.then(resolveNext, rejectNext)
             } else {
               resolveNext(res)
             }
           }
         } catch (error) {
           rejectNext(error)
         }
       }
 ​
       switch(_status) {
         case PROMISESTATUS.PENDING:
           this._fulfilledQueues.push(fulfilled)
           this._rejectedQueues.push(rejected)
         break
         case PROMISESTATUS.FULFILLED:
           fulfilled(_value)
         break
         case PROMISESTATUS.REJECTED:
           rejected(_value)
         break
       }
     })
   }

判断是否为函数,使用ts中的自定义类型收窄:

 export const isFunction = (val: unknown): val is Function =>
   typeof val === 'function'

ts中使用is语法进行类型收窄,如果返回结果为true,那么此分支会被推断为Function,判断Promise类型类似:

 export const isPromise = <T = any>(val: any): val is TinyPromise<T> => {
   return val instanceof TinyPromise
 }

至此,then方法的功能已经基本上实现了,而对于ts的类型定义还是非常简陋,可以借鉴PromiseLink来对我们的then方法进行更精确的约束:

 // then方法的返回值为Promise
 public then<TRes1 = T, TRes2 = never>(resolveInThen?: onFulfilled<T, TRes1>, rejectinThen?: onRejected<TRes2>):
     TinyPromise<TRes1 | TRes2> {}
 ​
 // then方法的参数可以为空
 // 如果不为空,则返回值可以为Promise
 type onFulfilled<T, TRes1> = ((value: T) => TRes1 | PromiseLike<TRes1>) | undefined | null
 type onRejected<TRes2> = ((err: any) => TRes2 | PromiseLike<TRes2>) | undefined | null

然后就可以执行一下本节开头的例子:

 第一个异步Promise 第一个then
 第二个异步Promise 第二个then

由于我们已经实现了then方法之间的参数传递,所以也实现了穿透的功能:

 new TinyPromise((reslove) => {
   reslove('hello')
 })
   .then()
   .then()
   .then()
   .then((res) => {
     console.log(res) // 'hello'
   })

四. 执行器传入Promise

问题又又又来了,如果想在执行器中传入Promise呢?就想这样:

 let p = new TinyPromise((res) => {
   setTimeout(() => {
     let t = new TinyPromise((res2, rej2) => {
       res2('异步1')
     })
     // 在resolve中传入一个promise
     res(t)
   }, 1000)
 }).then((val:any) => {
   console.log(val, '1');
 ​
   return new TinyPromise((res, rej) => {
     setTimeout(() => {
       res('异步2')
     }, 1000)
   })
 }, () => { }).then((val: any) => {
   console.log(val, '2');
 }, () => { })

此时我们希望在第一个定时器执行完毕后调用res传入Promise,然后将后续的then方法都挂载到这个Promise上。

此时就需要修改_resolve方法了:

 private _resolve: Resolve<T> = (resolveValue) => {
     const run = () => {
       if (this._status !== PROMISESTATUS.PENDING) return
 ​
       const runFulfilled = (value: any) => {
         let cb;
         while (cb = this._fulfilledQueues.shift()) {
           cb(value)
         }
       }
 ​
       const runRejected = (error: any) => {
         let cb
         while (cb = this._rejectedQueues.shift()) {
           cb(error)
         }
       }
       // 判断参数是否为Promise?
       if (isPromise(resolveValue)) {
         // 定义then方法,手动变更状态
         resolveValue.then((val) => {
           this._value = val
           this._status = PROMISESTATUS.FULFILLED
           runFulfilled(val)
         }, (err) => {
           this._value = err
           this._status = PROMISESTATUS.REJECTED
           runRejected(err)
         })
       } else {
         // 不为Promise,直接变更状态,执行回调函数
         this._value = resolveValue
         this._status = PROMISESTATUS.FULFILLED
         runFulfilled(resolveValue)
       }
     }
     // 直接将resolve函数变为异步
     setTimeout(run, 0);
   }

_resolve函数中判断函数是否为Promise?如果是的话,就手动指定then函数,变更状态。

_reject函数类似,只不过不需要处理这种情况了:

 private _reject: Reject = (error) => {
     if (this._status !== PROMISESTATUS.PENDING) return
 ​
     const run = () => {
       this._status = PROMISESTATUS.REJECTED
       this._value = error
       let cb;
       while (cb = this._rejectedQueues.shift()) {
         cb(error)
       }
     }
     setTimeout(run, 0)
   }

此时执行本节的例子:

 异步1 1
 异步2 2

五. 其他函数

catch

catch函数只要用于捕获错误,而本身又可以继续调用then函数,所以直接使用then函数来传递一个错误即可:

  catch<TRes>(onRejected: onRejected<TRes>): TinyPromise<T | TRes> {
     return this.then(null, onRejected)
   }

resolve

resolve函数是一个静态方法,通过Promise.resolve来调用,直接返回一个成功状态的Promise,如果参数不是Promise,也不自动进行转换:

 static resolve<T>(value: T | PromiseLike<T>): TinyPromise<T> {
   // 判断是否为Promise
   if (isPromise(value)) {
     return value;
   }
 ​
   return new TinyPromise(resolve => {
     resolve(value);
   });
 }

reject

reject函数返回一个失败状态的Promise

 static reject<T = never>(error: any): TinyPromise<T> {
   return new TinyPromise((resolve, reject) => reject(error))
 }

all

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

 const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

 static all<T>(list: T[]): TinyPromise<T[]> {
   let values: any[] = []
   // 计数
   let count = 0
   // 返回一个Promise,便于后续使用then方法
   return new TinyPromise((resolve, reject) => {
     for (let [k, v] of list.entries()) {
       // 此时的this为Promise自身
       this.resolve(v).then((res: any) => {
         // 使用下标,保证顺序
         values[k] = res
         count++
         // 遍历完成,全部then执行完毕后,调用then方法返回
         if (count === list.length) return resolve(values)
       }, (err: any) => {
         reject(err)
       })
     }
   })
 }

遍历数组,因为我们直接调用了Promise.resolve方法,在内部会直接将非Promise也会直接进行转换,为每个遍历值创建一个成功状态的Promise,并绑定then方法,当所有的值都被遍历完成后,then方法执行完毕后,count这个计数的值也会与数组的长度相等,此时返回结果数组。

完整代码

 type Resolve<T = any> = (resolveValue: T | PromiseLike<T>) => void
 type Reject = (rejectValue: any) => void
 ​
 type ExecutorType<T> = (resolve: Resolve<T>, reject: Reject) => void
 ​
 // 非函数 / 返回值/promise
 type onFulfilled<T, TRes1> = ((value: T) => TRes1 | PromiseLike<TRes1>) | undefined | null
 type onRejected<TRes2> = ((err: any) => TRes2 | PromiseLike<TRes2>) | undefined | null
 ​
 enum PROMISESTATUS {
   PENDING = 'pending',
   FULFILLED = 'fulfilled',
   REJECTED = 'rejected'
 }
 ​
 export const isFunction = (val: unknown): val is Function =>
   typeof val === 'function'
 ​
 export const isPromise = <T = any>(val: any): val is TinyPromise<T> => {
   return val instanceof TinyPromise
 }
 ​
 export default class TinyPromise<T = any> {
   public _status: PROMISESTATUS = PROMISESTATUS.PENDING
   public _value!: T | PromiseLike<T>
   private _fulfilledQueues: Resolve[] = []
   private _rejectedQueues: Reject[] = []
 ​
   constructor(executor: ExecutorType<T>) {
     try {
       executor(this._resolve.bind(this), this._reject.bind(this))
     } catch (error) {
       this._reject(error)
     }
   }
 ​
   private _resolve: Resolve<T> = (resolveValue) => {
     const run = () => {
       if (this._status !== PROMISESTATUS.PENDING) return
 ​
       const runFulfilled = (value: any) => {
         let cb;
         while (cb = this._fulfilledQueues.shift()) {
           cb(value)
         }
       }
 ​
       const runRejected = (error: any) => {
         let cb
         while (cb = this._rejectedQueues.shift()) {
           cb(error)
         }
       }
 ​
       if (isPromise(resolveValue)) {
         resolveValue.then((val) => {
           this._value = val
           this._status = PROMISESTATUS.FULFILLED
           runFulfilled(val)
         }, (err) => {
           this._value = err
           this._status = PROMISESTATUS.REJECTED
           runRejected(err)
         })
       } else {
         this._value = resolveValue
         this._status = PROMISESTATUS.FULFILLED
         runFulfilled(resolveValue)
       }
     }
     setTimeout(run, 0);
   }
 ​
   private _reject: Reject = (error) => {
     if (this._status !== PROMISESTATUS.PENDING) return
 ​
     const run = () => {
       this._status = PROMISESTATUS.REJECTED
       this._value = error
       let cb;
       while (cb = this._rejectedQueues.shift()) {
         cb(error)
       }
     }
     setTimeout(run, 0)
   }
 ​
   public then<TRes1 = T, TRes2 = never>(resolveInThen?: onFulfilled<T, TRes1>, rejectinThen?: onRejected<TRes2>):
     TinyPromise<TRes1 | TRes2> {
     const {
       _value,
       _status
     } = this
     return new TinyPromise((resolveNext, rejectNext) => {
 ​
       const fulfilled = (val: any) => {
         try {
           if (!isFunction(resolveInThen)) {
             resolveNext(val)
           } else {
             let res = resolveInThen(val);
             if (isPromise(res)) {
               res.then(resolveNext, rejectNext)
             } else {
               resolveNext(res)
             }
           }
         } catch (error) {
           rejectNext(error)
         }
       }
 ​
       const rejected = (error: any) => {
         try {
           if (!isFunction(rejectinThen)) {
             rejectNext(error)
           } else {
             let res = rejectinThen(error);
             if (isPromise(res)) {
               res.then(resolveNext, rejectNext)
             } else {
               resolveNext(res)
             }
           }
         } catch (error) {
           rejectNext(error)
         }
       }
 ​
       switch (_status) {
         case PROMISESTATUS.PENDING:
           this._fulfilledQueues.push(fulfilled)
           this._rejectedQueues.push(rejected)
           break
         case PROMISESTATUS.FULFILLED:
           fulfilled(_value)
           break
         case PROMISESTATUS.REJECTED:
           rejected(_value)
           break
       }
     })
   }
 ​
   catch<TRes>(onRejected: onRejected<TRes>): TinyPromise<T | TRes> {
     return this.then(null, onRejected)
   }
 ​
   static resolve<T>(value: T | PromiseLike<T>): TinyPromise<T> {
     if (isPromise(value)) {
       return value;
     }
 ​
     return new TinyPromise(resolve => {
       resolve(value);
     });
   }
 ​
   static reject<T = never>(error: any): TinyPromise<T> {
     return new TinyPromise((resolve, reject) => reject(error))
   }
 ​
   static all<T>(list: T[]): TinyPromise<T[]> {
     let values: any[] = []
     let count = 0
     return new TinyPromise((resolve, reject) => {
       for (let [k, v] of list.entries()) {
         this.resolve(v).then((res: any) => {
           values[k] = res
           count++
           if (count === list.length) return resolve(values)
         }, (err: any) => {
           reject(err)
         })
       }
     })
   }
 }