手写 Promise 最佳实践

191 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

实现一个 promise

核心描述

本模块中的代码为示例代码,如果想要更完善的版本,建议查看拓展中的 promise-polyfill 的代码实现。

  • 模拟实现一个 Promise 的核心

    • 支持构造函数中可以执行同步方法
    • 支持 .then 的链式调用,且支持 .then 中返回新的 Promise 对象,实现不同 then 中可以返回不同的值
    • 支持 .then 的链式调用均为异步(并不是说同步不能实现 .then 的异步执行逻辑,只是要保证 Promise A+ 规范)
    • callbacks 执行队列(数组)
    • 内部触发 callbacks 执行的时机,即 resolve 执行
  • 实现一个 Promise 的步骤

    • 先保证基础的构造函数中有 resolve 和 callbacks
    • 保证有 then 方法
    • 保证可以支持同步和异步的函数执行
    • 保证 then 中的方法是通过异步执行
  • 扩展 Promise 方法

    • .resolve 的实现
    • .reject 的实现
    • .catch 的实现
    • .finally 的实现
    • .all 的实现
    • .allSettled 的实现
    • .race 的实现
    • .any 的实现
  • 实现源码示例

    • 代码不保证能通过 Promise A+ 规范的所有 test,仅作学习使用
    • 代码中的扩展方法不保证均能正常使用,仅用于提供思路
    • 建议按照上述的 Promise 的核心,以及实现 Promise 的步骤去阅读,重点思考实现的核心点,而非细节点
    • 代码如下:
    // Promise 的3种状态的枚举
    const PROMISE_STATE = {
      PENEING: 'pending',
      FULFILLED: 'fulfilled',
      REJECTED: 'rjgected'
    }
    // 模拟 Promise 实现,重点关注“核心”部分,“拓展”部分仅供参考
    class MyPromise {
      callbacks = []
      // state 有三个状态,pending - 等待,fulfilled - 完成,
      state = PROMISE_STATE.PENEING
      value = null
      // 核心 构造函数
      constructor(fn){
        // fn 即 new MyPromise((resovle,reject)=>{}) 中的 (resolve,reject) => {} 方法,这个回调函数是同步执行的逻辑
        // 其实就是在使用调用 resolve 时,本质是在调用 MyPromise 中的 _resolve 方法
        // 在使用 reject 时,本质是在调用 MyPromise 中的 _reject 方法
        // 此方法作用是批量执行链式调用的 then 方法,并把调用 resolve 时的结果返回给下一次链式调用的回调
        fn(this._resolve.bind(this), this._reject.bind(this))
      }
    
      // 核心 MyPromise.then 的实现,链式调用
      then(onFulfilled,onRejected){
        // 此处的 resolve 其实就是 MyPromise 中的 _resolve ,
        // 用于处理当前 promsie 中完成状态的执行函数,本质就是 this._resolve.bind(this)
        // reject 同理
        // 这里有一个关键的理解,就是通过 _handle 的调用,将此处新返回的 new MyPromise() 的对象,与初始时的  new MyPromise() 也就是调用 .then 的那个对象关联起来了
        // 当如到了初始的 new MyPromise() 对象的 callbacks 中,这样才能在触发后续的 then 中的逻辑执行
        // 之所以要在此处重新返回一个新的 new MyPromise() 对象,是为了让后续 then 中能独立处理其自身的逻辑,否则后续 then 中的数据,都是初始时 new MyPromise 中返回的数据
        return new MyPromise((resolve, reject)=>{
          this._handle({
            onFulfilled: onFulfilled || null,
            resolve,
            onRejected: onRejected || null,
            reject
          })
        })
      }
    
      // 核心 方法执行
      _handle(callback){
        // 如果是 pending 状态,表示是异步逻辑,即还未开始执行 resolve \ reject 的方法
        // 因为只要在执行 resolve\reject 方法时,才会改变 MyPromise 内部的 state 状态
        // pending 状态不会立即执行,会将要执行的方法放入 callbacks 队列中,当调用者开始执行 resolve\reject 时才会一次触发 callbacks 的数据
        if (this.state === PROMISE_STATE.PENEING) {
          this.callbacks.push(callback)
          return 
        }
    
        // 跟进不同状态,选择不同执行的回调
        let cb = this.state === PROMISE_STATE.FULFILLED ? callback.onFulfilled : callback.onRejected
    
        // 如果不存在入参的方法,则直接执行 resolve或reject,如 .then().then((rst)=>{}),第一个 then() 即表示 cb 为空的状态
        if(!cb) {
          this.state === PROMISE_STATE.FULFILLED ? callback.resolve(this.value) : callback.reject(this.value)
          return
        }
        // onFulfilled 其实是 then 的入参执行函数
        // 此处的 try ... catch ,也保证了,MyPromise 如果执行出错,也不会崩溃
        let ret ;
        try {
          ret = cb(this.value)
        } catch(err) {
          ret = err
        } finally{
          this.state === PROMISE_STATE.FULFILLED ? callback.resolve(ret) : callback.reject(ret)
        }
      }
    
      // 核心,执行构造函数中的 resolve 方法
      // 当执行 Promise 中作为入参的 fn 方法时,满足条件执行 resolve 方法时
      // 本质是在执行 _resolve ,即 Promse.then 中收集的链式调用批量执行
      // 并将每次执行的 value 传递给下一次 then 中要调用的 resolve 入参
      _resolve(value){
        // 如果返回值也是 Promise 对象
        if (value instanceof MyPromise) {
          // 其实这里不用 call 重新绑定 then 的函数中的 this 指向,也是没有问题的
          value.then.call(value, this._resolve.bind(this), this._reject.bind(this))
          return
        }
        this.state = PROMISE_STATE.FULFILLED
        this.value = value
        // 添加一个 setTimeout 的延迟是因为如果 Promise 初始化时,是通过同步执行的方式调用的 resolve 方法
        // 则会导致 then 的链式调用还未开始执行,即 callbacks 数组的长度为空
        // 在 ES6 的 Promise 中,其实是通过微任务来实现异步执行,而非 setTimeout 的宏任务
        setTimeout(()=>{
          while(this.callbacks.length) {
            const callback = this.callbacks.shift()
            this._handle(callback)
          }
        })
      }
    
      // 核心,执行构造函数中的 reject 方法
      // 内部的 reject 处理
      _reject(error) {
        this.state = PROMISE_STATE.REJECTED;
        this.value = error
        setTimeout(() => {
          while(this.callbacks.length) {
            const callback = this.callbacks.shift()
            this._handle(callback)
          }
        })
      }
    
       // 拓展 catch
      catch(onError) {
        return this.then(null, onError)
      }
    
      // 拓展 finally
      finally(onDone){
        if(typeof onDone !== 'function') return this.then()
        return this.then(
          value => MyPromise.resolve(onDone()).then(()=> value),
          reason => MyPromise.resolve(onDone()).then(()=>{
            throw reason
          })
        )
      }
    
      // 拓展 resolve
      static resolve(value){
        if(value) {
          // 如果入参是 Promise 对象,则直接返回入参
          if(value instanceof MyPromise) {
            return value
          } // 如果入参是 thenable 类型的对象,则对其包装一层 Promise 
          else if (typeof value === 'object' && typeof value.then === 'function') {
            const then = value.then;
            return new MyPromise(resolve => {
              then(resolve)
            })
          } // 如果只是一个正常值数据,则直接返回该值
          else {
            return new MyPromise(resolve => resolve(value))
          }
        } // 如果入参没有数据,则通过 
        else {
          return new MyPromise(resolve => resolve())
        }
      }
      
      // 拓展 reject
      static reject(value){
        if(value && typeof value === 'object' && typeof value.then === 'function'){
          const then = value.then
          return new MyPromise((resolve, reject) => {
            then(reject)
          })
        } 
        else {
            return new MyPromise((resolve, reject) => reject(value))
        }
      }
     
    
      // 拓展 race
      static race(promiseList){
          return new MyPromise((resolve, reject)=>{
            for(let i=0; i< promiseList.length; i++){
              MyPromise.resolve(promiseList[i]).then((value)=>{
                return resolve(value)
              }, (error)=>{
                return reject(value)
              })
            }
          })
      }
    
      // 拓展 any
      static any(promiseList){
          const rejectedArr = []
          let rejectedTimes = 0
          return new MyPromise((resolve, reject) => {
            if(promise === null || promiseList.length === 0) {
              reject('无效的参数')
            }
            for (let i=0; i< promiseList.length; i++){
              let p = promiseList[i]
              if(p instanceof MyPromise){
                p.then((data)=>{
                  resolve(data) // 使用最先成功的结果
                },(err)=>{
                  rejectedArr[i]=err
                  rejectedTimes ++
                  // 如果失败了,则保存错误信息,当全失败时,any 才失败
                  if(rejectedTimes === promiseList.length){
                    reject(rejectedArr)
                  }
                })
              } 
              else {
                resolve(p)
              }
            }
          })
      }
    
      // 拓展 all
      static all(promiseList){
        return new MyPromise((resolve, reject) => {
          let fulfilledCount = 0
          const length = promiseList.length
          const rets = Array.from({length: length})
          promiseList.forEach((item ,index) => {
            MyPromise.resolve(item).then(result => {
              fulfilledCount ++
              rets[index] = result
              if(fulfilledCount === length) {
                resolve(rets)
              }
            }, reason => reject(reason))
          });
        })
      }
    
      // 拓展 allSettled
      static allSettled(promiseList){
        return new MyPromise(resolve => {
          const data =[], len = promiseList.length
          let count = len
          for (let i=0; i< len; i+=1){
            const promise = promiseList[i]
            promise.then(res=>{
              data[i] = {status: 'fulfilled', value: res}
            }, error =>{
              data[i] = {status: 'rejected', value: error}
            }).finally(()=>{
              if(!--count) {
                resolve(data)
              }
            })
          }
        })
      }
    
    }
    
    // 调用示例
    const p1 =new MyPromise(resolve=>{
      console.log('初始化 MyPromise')
      setTimeout(()=>{
        resolve('hello MyPromise!')
      })
      
    }).then(rst => {
      console.log('then 1 输出结果:', rst)
      return new MyPromise(resolve=>{
        setTimeout(()=>resolve('hello MyPromise then'),1000)
      })
    }).then(rst=>{
      console.log('then 2 输出结果:', rst)
    })
    
    // 输出示例
    // 初始化 MyPromise
    // then 1 输出结果: hello MyPromise!
    // then 2 输出结果: hello MyPromise then
    
    

知识拓展

  • Promise A+ 规范:如果想要深入理解 Promise ,则了解 Promise A+ 规范是最标准的答案,建议阅读参考资料中的英文原文或译文,再反过来去看自己实现的 Promise。
  • Promise 的 API,比了解 Promise 的实现更重要的是实际应用中的使用,所以需要更关注下面的 API:
    • Promise.all():接受一个 promise 对象的 iterable 类型(Array、Map、Set),返回一个 Promise 对象
      • 以输入为 promise 类型的数组为例,如果数组中的 promise 对象都正常返回了 resolve ,则会在所有 promise 都完成时,触发 Promise.all 方法,它会返回输入的所有 promise 的 resolve 回调的结果数组。
      • 如果输入的 promise 对象中有一个发生异常,或触发了 reject 回调,则 Promise.all 不会关系是否输入的 promise 对象都已经执行完成,而是直接抛出错误,并且 reject 的是第一个抛出的错误信息
    • Promise.allSettled():和 Promise.all 类似,唯一的区别是,此方法会等所有输入的 promise 对象都执行了 resolve 或 reject (或抛出异常),之后按输入顺序返回每个 promise 的结果。
    • Promise.any():入参是 promise 对象的 iterable 类型,返回结果是入参的 promise 中的第一个成功的 resolve 对象,这里的“第一个”表示的是第一个成功的 promise 对象,而非顺序上的第一个。同时会忽略其他成功或失败的返回结果。如果传入的 promise 对象都失败,则会返回异步失败和一个 AggregateError 对象,继承自 Error,有一个 errors 属性,属性值是由所有失败值填充的数组。(一个成功就成功,全部失败才失败)
    • Promise.prototype.catch():catch 方法可以用于您的 promise 组合中的错误处理。
    • Promise.prototype.finally():
      • 返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。
      • 在 finally() 方法后面继续 .then() ,这个 then 依然会执行,只不过无法拿到 finally 中 return 的数据
    • Promise.race():方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
    • Promise.reject():方法返回一个带有拒绝原因的 Promise 对象。
    • Promise.resolve():静态方法 Promise.resolve 返回一个解析过的 Promise 对象。
    • Promise.prototype.then():方法返回一个 Promise。它最多需要有两个参数:Promise 的成功和失败情况的回调函数。
  • promise-polyfill 核心实现:本来想贴代码,后来觉得不如直接去看代码库,所以感兴趣的话可以直接查看源码:github.com/taylorhakes…
    • 此库的源码更符合 Promise A+ 规范
    • 考虑的方面更全,所以会有很多边界判断
    • 可以直接拿来项目中使用,也具有学习价值
  • 其他
    • Promise 的链式调用过程中,如果某个 then 方法报错了,则会中断后续的 then 方法,直接跳到 catch 的处理方法中,但是 catch 后的 then 方法不受影响
    • Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
    • 一个 Promise 必然处于以下几种状态之一:
      • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
      • 已兑现(fulfilled): 意味着操作成功完成。
      • 已拒绝(rejected): 意味着操作失败。
    • Promise 需要支持 .then 的方法
    • 需要确保onFulfilled和onRejected异步地执行,并且应该在then方法被调用的那一轮事件循环之后用新的执行栈执行。这可以用如setTimeout或setImmediate这样的“宏任务”机制实现,或者用如MutationObserver或process.nextTick这样的“微任务”机制实现。
    • 每次 .then 之后的对象,是一个新的 Promise,这样才可以把每次 .then 中处理的数据再传递下去,否则传递的都是第一个 Promise 中返回的数据
    • 实现执行 Promise 构造函数中的同步方法的逻辑,之所以需要用类似 setTimeout 的异步方法包装,本质上是为了符合 Promise A+ 规范,如果不考虑这个规范对于同步方法异步执行的要求,也可以不用 setTimeout 来包一层,只需要用内部的 state 状态来区分即可
  • 杂谈:其实之所以我们花时间去学习 Promise 的实现,我思考下来,无非这几点:
    • 面试
    • 学习 ES5 到 ES6 新增这个 API 的历史逻辑
    • 我认为更重要的是,多花些时间去掌握 Promise 的 API ,以及它的执行时机,也许意义更大一些

参考资料

浏览知识共享许可协议

本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。