JS高级-手写Promise详解

1,234 阅读48分钟

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

  • 对该系列知识感兴趣和想要一起交流的可以添加wx:XiaoYu2002-AI,拉你进群参与共学计划,一起成长进步
  • 课程对照进度:JavaScript高级系列152-162集(coderwhy)
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力

脉络探索

  • 本章节中会根据Promise A+规范来手写一个基础的Promise实现过程,我们会不断优化改动,使其一步步趋向于完美
    • 在这个过程中,我们会说明其中为什么需要这么做,是出于什么目的以及设计理念
    • then方法是其中最主要的难点,后续由ECMA所实现的三个实例方法以及六个静态方法都是基于then方法去进一步封装扩展的语法糖
    • 因此当在阅读then手写思路方案感觉难度较大是正常的,等经过该阶段,手写后续方法就会感到足够的轻松与理解

Promise规范介绍

MDN文档对thenable的描述

图27-1 Promise A+规范

Promise 的标准化之路并非一帆风顺。在 Promise 被正式纳入 ECMAScript 规范之前,有很多不同的实现和行为模式。这些差异导致了兼容性问题和在不同环境下的不可预测行为,因此产生了对统一标准的需求,最终促成了 Promise A+ 规范的诞生

  • 在上一章节开头中,我们简单实现Promise之前的异步操作方式
    • 该异步操作主要依赖于回调函数(callback)进行信息传递。这种方式当代码量起来时,容易导致著名的“回调地狱”(Callback Hell)
    • 而回调模式的深层嵌套和错误处理的复杂性激发了JS社区对更好异步处理模式的探索
    • 最初,Promise 概念在社区中逐渐获得关注,并被多个库如 Q, when.js, Bluebird 等实现,但这些实现之间存在细微的差异。不同实现的 Promise 行为不一,比如错误处理、解决(resolve)和拒绝(reject)的具体细节,以及链式调用中的异步行为等
  • 随着 Promise 在实际应用中的普及,对于一个统一和可预测的 Promise 行为标准的需求变得迫切,开发者需要一套清晰、一致的规则,以确保不同的代码库和应用可以无缝集成,并在不同的执行环境中保持相同的行为
    • Promise A+规范在该需求以及环境下诞生,主要目标是提供一个最小的、可互操作的 Promise 设计,专注于提供一个简单而健壮的 then 方法,如图27-2
    • 规范详细描述了 then 方法的行为,包括如何处理值的传递、如何进行错误传递、以及 Promise 状态的变更过程。没错,只有then方法,因此我们上一章节反复强调finally和catch方法都不属于Promise A+规范的内容,这两个特殊方法只是ES6所实现then方法的一种语法糖,本质上依旧是then方法

MDN文档对thenable的描述

图27-2 专注于then方法的Promise A+规范

  • 因此Promise A+ 规范是通过广泛的社区合作和许多开发者的贡献完成的。它不是从顶层技术委员会或大公司推动,而是由社区中的实际用户和库的作者推动,确保规范符合实际开发需求
    • 后续 ECMAScript 正式规范中的 Promise 实现,即 ECMAScript 2015 (ES6) 中引入的 Promise, 在很大程度上吸收了 Promise A+ 规范的精髓,融汇成为 JavaScript 语言的正式部分
    • Promise A+ 的成功也展示了开源社区如何能够在缺乏官方指导的情况下,自发组织起来,解决共同的技术挑战,推动技术的发展
  • Promise A+规范文档地址:Promises/A+ (promisesaplus.com)

Promise设计和构造方法

  • 在本次手写Promise中,不会完全照搬Promise A+规范的内容,因为该规范具备大量的边界判断情况,我们主要实现最核心的主体部分,对于边界情况,大家如果感兴趣可以自行翻阅规范文档进行学习
  • Promise通过new调用进行使用,说明这是一个构造函数,进而创建Promise对象
    • 在这次实现中,我们给自定义实现取名为MyPromise
    • 构建方式有两种,ES5的function形式以及ES6的Class形式,我们在这里选择后者,逻辑会更清晰
  • 在通过class形式进行初步构建时,我们将传入的回调函数在一开始时直接调用,executor内部打印能够正常执行,说明初步构建成功
class MyPromise {
  constructor(executor) {
    executor()//立即调用
  }
}
//Promise的使用模式
const myPromise = new MyPromise((resolve, reject) => {
  console.log('JS高级-手写Promise');
})
  • 之所以需要new出来后立刻调用一次,是为了触发内部代码执行,改变当前的Promise状态,从pending进入fulfilled或rejected状态,进而实现信息传递
  • 因此我们还需要实现resolve以及reject两回调参数逻辑,逻辑内需要做到以下两点:
    1. 改变Promise状态
    2. 异步信息传递
  • 改变Promise状态主要目的有以下几点:
    1. 改变状态一旦锁死,接下来就无法继续调用resolve或reject,但可以继续执行正常代码
    2. 进入Promise阶段二,then方法能够正确对应接收resolve或reject回调信息
  • 三个状态,我们使用常量来进行存储(在TS中可以使用枚举)
    • 默认pending状态,执行resolve或reject方法回调时,判断当前状态为pending才进行执行,并改变状态
    • 当然,我们所说的状态锁死,不再调用,事实上还是会调用的,这是没有办法进行阻止的,但我们可以选择判断状态,当不符合状态时,调用空的内容,营造不可调用效果
// ES6 ES2015
// https://promisesaplus.com/
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    // 默认状态:pending
    this.status = PROMISE_STATUS_PENDING

    const resolve = () => {
      // 只有处于初始状态才能执行,状态已发生变化则锁死不执行
      if (this.status === PROMISE_STATUS_PENDING) {
        // 改变状态:fulfilled
        this.status = PROMISE_STATUS_FULFILLED
        console.log("resolve被调用")
      }
    }

    const reject = () => {
      // 只有处于初始状态才能执行,状态已发生变化则锁死不执行
      if (this.status === PROMISE_STATUS_PENDING) {
        // 改变状态:rejected
        this.status = PROMISE_STATUS_REJECTED
        console.log("reject被调用")
      }
    }

    executor(resolve, reject)
  }
}

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  resolve()//resolve调用,则reject不再调用
  reject()
})
  • 第二步,我们需要实现异步信息传递,当异步信息传递resolve和reject中,需要进行接收,因此我们继续使用两个变量value和reason来进行接收,该变量值默认为undefined。PS:变量名与默认值都是Promise规范所规定
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    //声明两个用于接收异步信息的变量
    this.value = undefined
    this.reason = undefined

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_FULFILLED
        //fulfilled状态,赋值对应异步信息
        this.value = value
        console.log("resolve被调用")
      }
    }

    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_REJECTED
        //rejected状态,赋值对应拒绝信息
        this.reason = reason
        console.log("reject被调用")
      }
    }

    executor(resolve, reject)
  }
}

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  resolve('成功调用resolve')//resolve调用,则reject不再调用
  reject()
})

then方法和执行顺序

所需信息都已经被存储,我们如果需要验证信息是否成功被传递,需要通过then方法调用,因此需要编写then方法,在这里暂时需要注意几点:

  1. 参数一(onFulfilled)与参数二(onRejected)的参数顺序问题,兑现在前,拒绝在后,并且参数命名规范中可以看出都以on为开头,这依旧是一个规范,普通适用于编程的各种情况,例如onclick点击等等,规范内容是:当某一时刻需要执行该内容
    • 该时刻即为跟在on后面的内容
    • 例如onclick的click为单击(点击),则该意思为当发生点击事件时进行调用,该规范在后续学习Vue的生命周期时会非常常见,即在某一个固定阶段会调用的方法,如下图27-3

MDN文档对thenable的描述

图27-3 Vue3的生命钩子(on开头)

  1. 第二点注意事项则是执行顺序,什么的执行顺序?
    • 我们先尝试来编写一下then方法,才能够清楚要探究的是什么的执行顺序
  • 在下方这个案例中,我们编写了then方法,并且采取存储then方法中的两个回调参数,将其藏在resolve和reject中进行回调,这个方式就像接力一样,异步信息经过中转成功传递
    • 我们先不真正传递信息,而是看onFulfilled和onRejected是否能够在resolve和reject中回调成功,是否可以成功在then方法调用的两个回调函数中打印出内存

MDN文档对thenable的描述

图27-4 Promise的异步信息回调原理

const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_FULFILLED
        this.value = value
        console.log("resolve被调用")
        this.onFulfilled()//then方法接收resolve状态传递的信息
      }
    }
    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_REJECTED
        this.reason = reason
        console.log("reject被调用")
        this.onRejected()//then方法接收reject状态传递的信息
      }
    }

    executor(resolve, reject)
  }
//编写then方法
  then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled
    this.onRejected = onRejected
  }
}

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  resolve('成功调用resolve')
  reject()
})

myPromise.then(res => {
  console.log(res);
}, err => {
  console.log(err);
})
  • 在该此调用中,会返回一个报错:TypeError: this.onFulfilled is not a function
    • onFulfilled不是一个函数?这又是为什么呢?我们在then方法中所传递的,确确实实是两个用作回调函数的箭头函数
    • 这就是我们要解释的执行顺序问题,在调用new MyPromise时,executor里的内容会立即执行,但根据JS引擎的从上到下执行顺序,执行resolve或者reject中的onFulfilled、onRejected时就完蛋了
    • 因为调用myPromise实例对象的then方法是注定会在new该实例对象的后面,所以此时调用resolve或reject时,内部的this.onFulfilled()、this.onRejected()都只是一个默认赋值的undefined,对于这一点是可以进行尝试的
if (this.status === PROMISE_STATUS_PENDING) {
  this.status = PROMISE_STATUS_FULFILLED
  this.value = value
  console.log("resolve被调用")
  //尝试打印回调函数onFulfilled
  console.log(this.onFulfilled);
}
  • 因此,在执行resolve和reject方法时,最好已经保证对应的then方法已经执行了,这样对应的onFulfilled和onRejected才有对应的回调函数可供resolve和reject这两个改变状态的类方法进行调用
    • 一旦要涉及该问题,就必须要说明到宏任务与微任务,但该内容还未进行学习,因此我们先不深入研究这一原理,但作为实现,我们可以先暂时使用setTimeout全局函数,在如图27-5的MDN文档描述中,该定时器是一个异步函数,并不会阻塞后续代码的执行
    • 意思是当我定时器在定时的时候,后续代码会继续执行,等到定时到达对应时间后再来执行该定时器内容
    • 所以我们可以将this.onFulfilledthis.onRejected使用定时器嵌套一层,则then方法的执行会先一步执行

MDN文档对thenable的描述

图27-5 MDN文档中对setTimeout定时器说明

//将resolve和reject中对应方法执行嵌套一层setTimeout
setTimeout(() => this.onFulfilled(this.value), 0)
setTimeout(() => this.onRejected(this.reason), 0)
  • 在嵌套一层0秒的定时器后,其结果能够正常进行返回,但使用定时器只是权宜之计,并不是什么好的设计方式,因为setTimeout是一个宏任务,而真正的Promise在执行这一步时,是一个微任务
    • 因此我们使用Window接口的queueMicrotask方法来替代定时器,该方法是一个微任务,如图27-6
    • 在使用角度上没有区别,但本质上有较大差别,在后续我们会再专门说明
    • 同时需要注意,状态的锁定不能加入微任务内,因为myPromise实例对象一开始会立即执行,执行到对应改变状态方法时,一旦延后锁死状态,在当轮的resolve与reject内部判断条件中,状态都暂时还是pending,则两个类方法都会执行,等当下执行完才在下一轮中改变状态,则失去锁定状态的意义

MDN文档对thenable的描述

图27-6 微任务执行的queueMicrotask方法

const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_FULFILLED
        this.value = value
        console.log("resolve被调用")
        //微任务执行该onFulfilled回调,晚于then方法执行
        queueMicrotask(() => this.onFulfilled(value))
      }
    }

    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_REJECTED
        this.reason = reason
        console.log("reject被调用")
        //微任务执行该onRejected回调,晚于then方法执行
        queueMicrotask(() => this.onRejected(reason))
      }
    }

    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled
    this.onRejected = onRejected
  }
}

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  resolve('成功调用resolve')
  reject()
})

myPromise.then(res => {
  console.log(res);
}, err => {
  console.log(err);
})
  • 作为初步的Promise调用是能够进行运行的,但Promise还有多种调用方式是我们尚未处理的,例如:

    1. 调用then方法不传参数或者只传参数一或者传入的是Promise

      • 一旦我们的边界处理没有妥当,例如在executor中触发reject方法,但then没有参数二onRejected进行回调处理或边界处理,则会报错

      • 对于该点,我们完全可以在执行这两回调函数前,先进行if判断该回调函数是否存在,存在再调用,其余输入情况,则可以参考Promise A+规范或者MDN文档中对于Promise then的返回值界定或者直接返回

    2. 多次调用then方法,应该多次执行,但我们目前的代码只会执行单次

      • 因为我们采用的是赋值this.onFulfilled和onRejected的形式,因此第二个then会覆盖第一个then的内容
      • 所以可以采用数组进行存储该调用,有多少次调用then方法,则执行多少次
    3. 当前我们是无法进行链式调用的,不管是链式then方法还是catch、finally方法

      • catch、finally方法是因为还未实现,而无法链式then方法则是因为我们所返回的结果就真的只是结果
      • 在一个已经确定的结果上,是没有then方法可以调用的,因此所返回的结果应该包裹一层属于我们的Promise,才能够从对应的原型中拿到then方法继续调用

MDN文档对thenable的描述

图27-7 Promise对应then方法应该处理的返回值

then方法的优化

  • 优化1:then多次调用,就会多次执行,采用数组存储,每次调用将其存储进数组中,从数组中遍历调用
    • 因此我们只需要提前声明两个数组,一个用于存放fulfilled状态的调用,另一个用于存放rejected状态的调用
    • 每次调用then方法时,都会push到对应的数组内,然后在下一个微任务中统一执行
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined
    // 存储then方法对应的执行次数
    this.onFulfilledFns = []
    this.onRejectedFns = []

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_FULFILLED
        this.value = value
        //对累积的then调用兑现状态函数进行遍历分别调用
        queueMicrotask(() => {
          this.onFulfilledFns.forEach(fn => fn(this.value))
        })
      }
    }

    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_REJECTED
        this.reason = reason
        //对累积的then调用拒绝状态函数进行遍历分别调用
        queueMicrotask(() => {
          this.onRejectedFns.forEach(fn => fn(this.reason))
        })
      }
    }

    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    //将成功的回调和失败的回调放到数组中,统一对应调用
    this.onFulfilledFns.push(onFulfilled)
    this.onRejectedFns.push(onRejected)
  }
}

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  reject('成功调用reject')
})

myPromise.then(res => {
  console.log("res1:", res)
}, err => {
  console.log("err1:", err);
})
myPromise.then(res => {
  console.log("res2:", res)
}, err => {
  console.log("err2:", err);
})
  • 但接下来就容易产生另一个问题,我们是将多次then调用收集到数组中,在确定Promise状态后,要是在定时1s的定时器中继续调用then方法
    1. 该定时器内的then方法应不应该加入该onFulfilledFnsonRejectedFns数组中?
    2. 加入后,每次都会和其他then方法相差1s执行,会不会导致该then方法不被调用?
// 在确定Promise状态之后, 再次调用then
// 该调用是否能够正常执行?
setTimeout(() => {
  myPromise.then(res => {
    console.log("res3:", res)
  }, err => {
    console.log("err3:", err)
  })
}, 1000)
  • 这个方法是应该加入对应数组中的,但在我们加入后,并没有有执行定时器内的代码,但没有生效,这是为什么呢?
    • 因为当发生异步之后,我们的onFulfilledFns和onRejectedFns的forEach遍历是在下一个微任务中进行的,该时间早于定时器的1s
    • 当定时器触发进行调用时,该then方法虽然加入对应存储数组中,但状态已经锁死,不再触发resolve或reject,且错过了回调函数组的遍历执行
    • 但在ES6的Promise中是能够执行的,这要怎么实现?
const promise = new Promise((resolve, reject) => {
  resolve('aaaa')
})

promise.then(res => console.log('res1:', res))
promise.then(res => console.log('res2:', res))
promise.then(res => console.log('res3:', res))

//停顿1s后,会继续输出该内容
setTimeout(() => {
  promise.then(res => console.log('res4:', res))
}, 1000)
  • 此时若想要达到该效果,则需要灵活运用Promise的三个状态判断,在状态已经锁死的情况下,对后续而来的then方法进行"补票"调用
    • pending状态时:正常处理,将失败与成功的数组添加到回调函数组内
    • fulfilled状态时:直接执行then方法内的onFulfilled回调内容,并传递数据
    • rejected状态时:直接执行then方法内的onRejected回调内容,并传递数据
then(onFulfilled, onRejected) {
  // 1.如果在then调用的时候, 状态已经确定下来
  if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
    onFulfilled(this.value)
  }
  if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
    onRejected(this.reason)
  }

  // 2.将成功回调和失败的回调放到数组中
  if (this.status === PROMISE_STATUS_PENDING) {
    this.onFulfilledFns.push(onFulfilled)
    this.onRejectedFns.push(onRejected)
  }
}
  • 同时需要注意,一旦以状态为主,一开始resolve与reject方法内的改变状态代码就需要放置到异步方法queueMicrotask
    • 因为queueMicrotask微任务之内的代码调用会稍迟一步,由同步代码先进行执行
    • 如果改变状态部分像一开始时放在微任务外,一旦同步代码先执行,则状态先改变,后调用then方法。此时then方法中已经以判断状态为主进行处理,改变成锁死的状态,将无法执行正常pending状态判断内的逻辑代码,进而无法输出所需内容
    • 因此需要将改变状态放入微任务内,执行then方法中对应回调函数后,再锁死状态,防止状态对应不上,无法通过判断
const resolve = (value) => {
  if (this.status === PROMISE_STATUS_PENDING) {
    // 添加微任务
    queueMicrotask(() => {
      //该状态改变需要放入微任务内,在执行完then方法中的pending逻辑代码后再锁定状态
      this.status = PROMISE_STATUS_FULFILLED
      this.value = value
      this.onFulfilledFns.forEach(fn => {
        fn(this.value)
      })
    });
  }
}

const reject = (reason) => {
  if (this.status === PROMISE_STATUS_PENDING) {
    // 添加微任务
    queueMicrotask(() => {
      //该状态改变需要放入微任务内,在执行完then方法中的pending逻辑代码后再锁定状态
      this.status = PROMISE_STATUS_REJECTED
      this.reason = reason
      this.onRejectedFns.forEach(fn => {
        fn(this.reason)
      })
    })
  }
}
  • 优化2:调用then方法时,传递参数不定的判断:
    • 如果没有在 queueMicrotask() 的回调中添加状态检查,可能会出现在状态已经被一个 resolve()reject() 更改后,另一个 resolve()reject() 再次尝试更改状态的情况。这种情况可能发生在复杂的异步流程中,或者当多个操作都试图解决同一个 Promise 时
    • 确保状态一次性改变是 Promise 设计的关键要求,如果未能确保则会导致不符合预期的执行,如上方代码会resolve与reject同时执行或者同时多次执行
  • resolve与reject的第一层限制状态是限制同步操作下的情况,而优化2的限制处理则是微任务(异步)下的限制
    • 一旦当我们的then初始调用代码位于同步代码中,状态锁死位于异步代码中,会出现resolve与reject都能正常突破第一层限制状态,因为此时尚未锁死状态,等到完成同步代码会执行异步代码,此时已经突破一层限制,没有二层限制异步,则resolve与reject方法都会执行
    • 因此在queueMicrotask执行逻辑代码之前,需要在最前面再加一层限制措施,不符合则返回,不执行后续内容
queueMicrotask(() => {
  //添加在原有代码前面
  if (this.status !== PROMISE_STATUS_PENDING) return
})
  • 优化3:令then方法后面能够进行链式调用
    • 我们当前代码无法进行链式调用,主要原因在前面有进行说明
    • 处理方式为返回值需要是一个myPromise实例对象,才能够调用prototype上的then方法
  • 根据MDN文档在then方法中返回值的界定中,有六种返回结果:
    • 1、正常返回值 2、undefined,没有返回值 3、返回错误
    • 4、5、6、返回三个阶段(兑现、拒绝、待定)的Promise
    • 其中1、2、3需要通过Promise包裹一层,才能够实现链式调用
myPromise.then(res => {
  console.log("res1:", res)
}).then(res => {
  console.log("res2:", res)
})
//上方链式调用等价于以下代码
let myPromise1 = myPromise.then(res => {
  console.log("res1:", res)
})

myPromise1.then(res => {
  console.log("res2:", res)
})
  • 需要修改的位置很明确,即返回调用内容的位置
    • 在手写then方法中,该执行位于两个存储回调函数数组的遍历执行中,并将其异步内容传递进去
//执行位置
this.onFulfilledFns.forEach(fn => fn(this.value))
this.onRejectedFns.forEach(fn => fn(this.reason))
  • 我们的结果无非是函数执行的情况fn(this.value)或者fn(this.reason),我们能否使用一个变量来接收该结果然后包裹一层Promise之后进行返回?
    • 如果需要做到该要素,则需要继续进行变量管理,而经过重重回调的变量是难以管理的,不好实现我们的想法
    • 如果该位置无法实现,我们就可以继续往上追溯回调,去沿着这一条脉络探索,看其是否可以实现
    • 能够追溯到往两个存储回调函数数组push回调函数的地方,在这里是接收开发者编写的回调函数最前线,在这里也许更好操作
if (this.status === PROMISE_STATUS_PENDING) {
  //存储开发者编写的then方法回调函数的最前线
  this.onFulfilledFns.push(onFulfilled)
  this.onRejectedFns.push(onRejected)
}
  • 在这里传递进对应回调前,也许可以对该回调进行处理,进行包裹,则当流程走到遍历执行的过程时,就已经是Promise包裹状态,因此在这里我们做出两个处理:
  1. 传入两个存储回调函数数组中的回调函数会再嵌套一层回调,在该回调中首先获取值,其次包裹值。在获取值的同时会执行该函数,一举两得,一旦该内容由开发者手动抛出内容,则会被resolve或者reject方法再次接住,相当于使用Promise.resolve()或者Promise.reject()将普通值转为Promise,则该返回值可以调用Promise对应的实例方法then
  • 在执行then方法的同时,对获取的值进行包裹一层我们自己的Promise
then(onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {
    //其余内容暂时省略...
    //在then方法中包裹一层属于自身的Promise
    if (this.status === PROMISE_STATUS_PENDING) {
      this.onFulfilledFns.push(() => {
        const value = onFulfilled(this.value)
        resolve(value)
      })
      this.onRejectedFns.push(() => {
        const reason = onRejected(this.reason)
        return resolve(reason)
      })
    }
  })
}
  • 则返回值有以下两种情况:
    • 该形式和Promise.resolve()Promise.reject()的效果一致,是其完整写法,也就相当于在值的基础上包裹一层MyPromise
    • 因此内层调用回调函数获取返回值,外层Promise包裹其返回值并调用对应的状态方法,从而实现值的Promise化,做到能够继续链式调用对应方法
//情况1
new MyPromise((resolve, reject) => {
  resolve('resolve')
})
//情况2
new MyPromise((resolve, reject) => {
  resolve('reject')
})
  1. Promise在then方法中一旦遇到抛出错误后一样会进行处理,一旦遇到该情况则抛出,交给下一层链式调用的rejected状态处理。该处理流程我们使用try...catch处理,首先执行 try 块中的代码,如果它抛出异常,则将执行 catch 块中的代码
    • 且该抛出异常处理不仅可以用在判断待定状态的处理中,也适用于兑现、拒绝状态的处理,因为三种状态对值传递的方式是相同的,区分为三种状态是为了保证状态锁死后,后续调用then方法依旧能够执行
    • 在这里分为了同步情况处理以及异步情况处理,但抛出异常的错误处理一致,所以该try...catch代码能够运用多处,包括但不限于当前场景
    • 保证了错误的处理,让Promise按预期方式传递
if (this.status === PROMISE_STATUS_PENDING) {
  this.onFulfilledFns.push(() => {
    try {
      //获取值
      const value = onFulfilled(this.value)
      resolve(value)
    } catch(err) {
      reject(err)
    }
  })
  this.onRejectedFns.push(() => {
    try {
      //获取值
      const reason = onRejected(this.reason)
      resolve(reason)
    } catch(err) {
      reject(err)
    }
  })
}
  • 在以上代码中,一旦throw new Error('err message')等错误信息,为了确保则能够被try...catch接收,并正确传递给reject方法处理
    • 以上是对阶段二的异常处理,而在阶段一的抛出异常也需要进行接收处理
    • 阶段一的内容都位于executor内,则类调用该回调时同时进行捕获
try {
  executor(resolve, reject)
} catch (err) {
  reject(err)
}
  • 为了减少try...catch的代码重复率,我们封装一个函数来实现抽取效果,共性部分为try..catch处理,需要我们传入的内容为:
    1. execFn (执行函数):负责执行具体的业务逻辑,其返回值或抛出的异常将决定 Promise 的下一步状态(即解决或拒绝)
    2. value (执行函数对应的参数值):连接 Promise 链各个环节的数据,确保上一个操作的输出可以作为下一个操作的输入
    3. resolve (解决函数)与reject (拒绝函数)作用不重复
  • 对于该工具函数的封装可以随着需求的增加而逐渐丰富处理情况,或者实现一个系列的异常处理工具函数
// 工具函数
function execFunctionWithCatchError(execFn, value, resolve, reject) {
  try {
    const result = execFn(value)
    resolve(result)
  } catch(err) {
    reject(err)
  }
}
  • 则经过处理后的代码如下:
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

// 工具函数
function execFunctionWithCatchError(execFn, value, resolve, reject) {
  try {
    const result = execFn(value)
    resolve(result)
  } catch(err) {
    reject(err)
  }
}

class MyPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledFns = []
    this.onRejectedFns = []

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        // 添加微任务
        queueMicrotask(() => {
          if (this.status !== PROMISE_STATUS_PENDING) return
          this.status = PROMISE_STATUS_FULFILLED
          this.value = value
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)
          })
        });
      }
    }

    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        // 添加微任务
        queueMicrotask(() => {
          if (this.status !== PROMISE_STATUS_PENDING) return
          this.status = PROMISE_STATUS_REJECTED
          this.reason = reason
          this.onRejectedFns.forEach(fn => {
            fn(this.reason)
          })
        })
      }
    }

    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      // 1.如果在then调用的时候, 状态已经确定下来
      if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
        execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
      }
      if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
        execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
      }

      // 2.将成功回调和失败的回调放到数组中
      if (this.status === PROMISE_STATUS_PENDING) {
        this.onFulfilledFns.push(() => {
          execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
        })
        this.onRejectedFns.push(() => {
          execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
        })
      }
    })
  }
}

const promise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  // resolve(1111) // resolved/fulfilled
  reject(2222)
  // throw new Error("executor error message")
})

// 调用then方法多次调用
promise.then(res => {
  console.log("res1:", res)
  return "aaaa"
  // throw new Error("err message")
}, err => {
  console.log("err1:", err)
  return "bbbbb"
  // throw new Error("err message")
}).then(res => {
  console.log("res2:", res)
}, err => {
  console.log("err2:", err)
})
  • 在满足当下之后,我们还可以在原有基础上继续增添更多的边界判断,例如:
    1. 当传入的不是值,而是一个Promise时,以当前Promise为返回值
    2. 当传入的是带then方法的对象,也就是thenable时
  • 这些都又要如何进行实现?这就需要在 resolve 函数中添加额外的逻辑来检测这些情况
    • 如果 valueMyPromise 的实例,我们直接使用 then 方法来处理这个 Promise。通过调用 then(resolve, reject),当前 Promise 的解决将依赖于 value Promise 的状态
    • 如果 value 是一个带有 then 方法的对象或函数(即 thenable),尝试按 Promise-like 的方式处理它,如图27-8,如下图,我们通过一个标志变量 called 来确保的不被多次调用

MDN文档对thenable的描述

图27-8 Promise A+规范中关于处理thenable的情况说明

class MyPromise {
  constructor(executor) {
    // 省略其他部分...

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        // 检测 value 是否为 Promise
        if (value instanceof MyPromise) {
          // 如果 value 是 Promise,等待它兑现或拒绝
          value.then(resolve, reject);
          return;
        }

        // 检测 value 是否为 thenable
        if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
          let then;
          try {
            then = value.then;
          } catch (error) {
            reject(error);
            return;
          }

          // 如果 then 是函数,认为 value 是一个 thenable 对象
          if (typeof then === 'function') {
            let called = false; // 避免多次调用
            try {
              then.call(value, (y) => {
                if (called) return;
                called = true;
                resolve(y);
              }, (r) => {
                if (called) return;
                called = true;
                reject(r);
              });
            } catch (error) {
              if (called) return;
              called = true;
              reject(error);
            }
            return;
          }
        }
      }
    }

    // 省略其他部分...
  }

  // 省略其他部分...
}
  • 那到目前为止,then方法的主要枝干部分就已经完成,我们以表格形式总结以上主要的几个迭代历程供大家梳理脉络
  • 接下来我们会根据已有的then方法,继续拓展ES6中Promise的特殊方法catch以及finally


表27-1 Promise then方法迭代过程

迭代步骤描述目的或结果
初始实现实现基础的 then 方法,简单存储并执行 onFulfilledonRejected 回调提供基本的异步成功和失败回调处理
状态管理引入状态控制,确保 resolvereject 只能改变状态一次防止多次调用和状态改变,保持 Promise 状态的不变性
异步执行使用 queueMicrotask 确保回调的异步执行使得 then 方法中的回调符合微任务队列的异步特性,符合规范
链式调用支持修改 then 方法以返回一个新的 MyPromise 实例允许 then 方法链式调用,每次调用返回新的 Promise 实例
处理返回值then 回调中处理返回值,尤其是处理 Promise 和 thenable 对象支持 Promise 解析过程,使得返回值可以是 Promise 或 thenable 对象
错误处理引入 try...catch 来捕获并处理回调中抛出的异常提高错误管理能力,将执行错误转化为 Promise 的拒绝状态
边界条件处理添加对传入 then 的非函数参数的处理,如忽略或空执行符合 Promise A+ 规范,对不合规的参数提供容错处理

手写catch方法

  • Promise 实例的 catch() 方法用于注册一个在 promise 被拒绝时调用的函数。它会立即返回一个等效的 Promise对象,这可以允许我们链式调用其他 promise 的方法。此方法是 Promise.prototype.then(undefined, onRejected) 的一种简写形式
  • 以上是MDN文档对catch方法的说明,非常清晰明确,我们可以对其进行需求分析,将其拆解为一个个步骤:
    1. promise在executor中调用reject方法,锁定rejected状态时调用的函数
    2. 返回等效Promise对象,支持链式调用
    3. then方法的简写形式(语法糖)
const promise = new Promise((resolve, reject) => {
  reject('rejected状态')
})
//catch方法主要使用形式:链式
promise.then(undefined).catch(err => {
  console.log(err);
})
  • 相对于then方法,catch方法更专一,从该基础说明上,实现难度小,只需要将then方法进行一定程度的封装即可包装为catch方法
    • 因此将then方法进行一定程度的简写封装并不是catch方法的主要难点
    • 以下的catch封装是正确的,但加入后还无法进行运行,主要在于主要难点还未解决
catch(onRejected) {
  this.then(undefined, onRejected)
}
  • 而catch方法主要难点位于以下几点:
    1. 返回的是等效Promise,什么是等效?和then方法所返回的是新的Promise有什么区别?
    2. 在实现catch方法时,与then方法中会有哪些边界问题冲突需要解决?
  • 首先说明第二点,冲突问题在于catch方法所要替代的是then方法的第二参数,而在调用then方法时,一旦不传入第二参数回调内容,则默认值为undefined,对于then方法来说,第二参数目前无法处理内容为undefined的情况
    • 因此在初始调用then方法回调时,需要先判断当前回调是否存在,只将回调参数存在的情况添加到回调函数调用组之中
if (this.status === PROMISE_STATUS_PENDING) {
  //判断onFulfilled是否存在
  if (onFulfilled) this.onFulfilledFns.push(() => {
    execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
  })
  //判断onRejected是否存在
  if (onRejected) this.onRejectedFns.push(() => {
    execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
  })
}
  • 对此,当onFulfilled或者onRejected为undefined时,也就是调用then方法未输出内容的情况,需要进行边界处理
    • 对onRejected的undefined处理,是catch方法能够运作的重要节点,但我们要如何处理?
    • 如何在then未处理onRejected的情况下,将该错误作为返回值传递给接下来链式调用的catch进行处理?
    • 在onRejected为空时,调用this.then(undefined, onRejected)是否可行?不行,因为onRejected为空不意味onFulfilled为空,这忽略对onFulfilled状态的处理情况
  • 那能否在then方法执行时,判断参数2的onRejected是否为undefined,若为undefined则调用catch方法中传入的onRejected?
    • 这是能够做到的,将promise2中传入的onRejected传到promise1中对应位置,但如果我们这样去管理的话,代码是非常难管理的
    • then 方法中直接处理 catch 的逻辑,意味着 then 需要知道 catch 的具体实现细节。这增加then与catch之间的耦合度,因此 then 方法不仅要处理其正常的功能,还要考虑错误处理的特殊逻辑。这违背了单一职责原则,即一个函数或模块应该有且只有一个改变的理由
  • 那需要如何实现当onRejected为undefined时,将错误传递下去?
    • 只需要抛出错误即可,错误会被try...catch所捕获进而抛出
    • 在then方法的最前列先判断onRejected是否存在,如果不存在则抛出错误,将直接跳过then的执行,进入能够处理Rejected状态的方法中
  then(onFulfilled, onRejected) {
    const defaultOnRejected = err => { throw err }
    //使用方式1
    onRejected = onRejected || (err => { throw err })
    //使用方式2
    onRejected = onRejected || defaultOnRejected
    
    const defaultOnFulfilled = value => { return value }
    onFulfilled = onFulfilled || defaultOnFulfilled
  }
  • 当完成第二点的难点后,对于第一点的难点也能够迎刃而解:
    • 我们需要返回的是附带拒绝信息和状态的Promise,而非一个全新的Promise
    • 即两个或者多个一致的Promise(状态一致、值相同、副作用一致),称为等效Promise
  • 最后,我们的catch一样支持链式调用,这意味着需要有Promise形式的返回值,该要求在then方法中已经实现成功,因此我们直接返回即可
catch(onRejected) {
  return this.then(undefined, onRejected)
}

手写finally方法

  • 手写finally方法的过程和思路与catch方法类似,我们依旧先来看下finally方法的MDN文档介绍:

    Promise实例的 finally() 方法用于注册一个在 promise 敲定(兑现或拒绝)时调用的函数。它会立即返回一个等效的Promise对象,这可以允许我们链式调用其他 promise 方法

    • 通过后续的说明,finally() 方法类似于调用 then(onFinally, onFinally),也是语法糖的简写形式,但有以下几点不同需要注意:

MDN文档对thenable的描述

图27-9 Promise中finally与then的不同之处

  • 该调用形式,finally作为最后的接收处理,接收到的是fulfilled状态还是rejected状态,都是正常情况,相当于对then方法的两参数回调都一视同仁进行处理,而这也是我们的目的
    • 而回调的调用只有两种方式,在判断三个状态中,分为同步处理和异步处理,这是前面有说明的
    • 同步处理是加入数组中遍历调用,异步处理则直接调用。所有的then调用都绕不开这一层状态判断限制处理
    • 因此catch方法和finally方法可以放心的使用基于then方法的调用,因为它们共享相同的基础逻辑和状态管理机制
finally(onFinally) {
  //两种写法选其一
  //then(onFinally, onFinally)
  this.then(() => {
    onFinally()
  }, () => {
    onFinally()
  })
}
  • 此时还存在一个问题,若在then与finally之间,间隔了一层catch处理,finally就无法进行执行
    • 虽然我们已经实现链式调用,但then一旦进行执行,说明进入的是fulfilled状态,则不会触发catch方法,因此then方法无法隔着没有进行调用的catch将返回内容传递给finally,也就无法正常调用
    • 反之若直接进入rejected状态,则执行catch方法能够继续顺序链式调用后续的finally方法
promise.then(res => {
  console.log("res1:", res)
  return "aaaaa"
}).then(res => {
  console.log("res2:", res)
}).catch(err => {
  console.log("err:", err)
}).finally(() => {
  console.log("finally")
})
  • 此时需要我们进行分析,要如何解决该问题:
    1. finally 方法的设计意图是无论 Promise 最终状态如何(不论是兑现还是拒绝),都应该被执行
    2. 最终值与状态需要传递到finally身上
    3. 如果在某个 catch 中没有修改错误(即没有返回新的值或抛出新的错误),它会“透传”这个错误到链中的下一个 catch。这样设计可以保证,即使某个 catch 不处理当前的错误,finally 依然可以被执行
  • 目前then方法是在onRejected参数未处理的情况抛出异常,由catch所接收,而该异常一样能够被finally接收
    • 在三个实例方法中,通常遵循先then后catch最后finally的处理方式,可以省略前者但最好不跨越到前者之前
    • 因此我们只需要考虑catch未执行的情况下,如果做到then的值能够继续传递下去且finally正常执行,如图27-10

MDN文档对thenable的描述

图27-10 Promise值传递并执行的过程

  • catch方法中的onFulfilled空执行是一定确定的,是undefined,已经在前面进行处理,所以我们需要跳过catch,在then方法的角度去下功夫
    • 继续在then方法最前列进行判断,一旦走then方法的onFulfilled回调,则会有对应的value值
    • 当then方法接下来是链式catch方法时,onFulfilled会注定为undefined,判断当为undefined时,赋值为value并返回,完成值的传递
    • 在catch方法中的执行中,相当于从return this.then(undefined, onRejected)变为return this.then(value, onRejected),状态已经锁定为fulfilled,onRejected不会执行,实质上相当于return this.then(value => resolve(value)),等同return一个Promsie.resolve(value)
    • 因此最后的finally所接收到的是一个由fulfilled返回值所包裹的Promise,该问题解决
  then(onFulfilled, onRejected) {
    //为空则为进入catch方法阶段,保证fulfilled状态的值成功传递
    const defaultOnFulfilled = value => { return value }
    onFulfilled = onFulfilled || defaultOnFulfilled
  }
  • 且由于我们对空值进行判断且处理,后续不存在空值情况,因此后续对onFulfilled和onRejected的空值判断就可以去除,但进行保留也不会造成其余影响
  • 到目前为止,Promise的三个实例方法就已经完成,我们接下来将进行实现其余六个静态方法

Promise静态方法

  • 作为类方法(静态方法),在类中的表现形式以static为开头,使用无需new调用
    • 静态方法在Promise A+规范中都是不存在的,都是基于ES6基于原有内容进一步封装的类似语法糖形式,方便使用

手写resolve/reject方法

  • resolve与reject静态方法的都是将传入的值包裹为Promise值,不同之处在于包裹后的Promise状态不同
    • 用于快速创建并设定 Promise 状态
    • 作为实现,其余边界处理都已经在之前完成,我们只需要将包裹后的值返回出去,作为后续链式调用依靠
  static resolve(value) {
    return new MyPromise((resolve) => resolve(value))
  }

  static reject(reason) {
    return new MyPromise((resolve, reject) => reject(reason))
  }
//测试
MyPromise.resolve("Hello World").then(res => {
  console.log("res:", res)
})
MyPromise.reject("Error Message").catch(err => {
  console.log("err:", err)
})

手写all/allSettled方法

  • all方法是聚合多个 Promise 的结果
    • 逻辑与&&类似的思想,所有内容都fulfilled才一起返回对应内容,中途有rejected直接返回rejected内容
//测试数据
const p1 = new Promise((resolve) => {
  setTimeout(() => { resolve(1111) }, 1000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => { reject(2222) }, 2000)
})
const p3 = new Promise((resolve) => {
  setTimeout(() => { resolve(3333) }, 3000)
})
//p2触发reject,则直接返回p2对应的reject内容
Promise.all([p1,p2,p3]).then(value => {
  console.log('value:',value);
}).catch(err => console.log('err:',err))
  • 可以看到all方法所传递进一个数组,而通过链式调用then方法,可以看出返回一个Promise
    • 因此可以初步得出all方法接收一个数组形式的参数,遍历该数组参数内的Promise进行调用,与我们在then方法中实现的多个then调用会多次生效具备一定相似性
  • 在遍历过程需要处理各种边界判断,例如:判断是否存在调用reject方法的情况
    1. 存在reject则直接调用
    2. 存在resolve则使用数组存储该结果,直到存储所有遍历resolve结果后,进行返回
static all(promises) {
  // 问题关键: 什么时候要执行resolve, 什么时候要执行reject
  return new HYPromise((resolve, reject) => {
    //存储所有遍历resolve结果
    const values = []
    promises.forEach(promise => {
      promise.then(res => {
        //收集所有成功结果
        values.push(res)
        //promise全部正常调用resolve,返回存储所有内容的结果
        if (values.length === promises.length) {
          resolve(values)
        }
      }, err => {
        //有一个拒绝状态,则直接返回err内容
        reject(err)
      })
    })
  })
}
  • allSettled方法的整理步骤与all方法极其相似,不同之处在于判断逻辑不同:
    1. 存储所有内容情况进行遍历
    2. 区分resolve与reject状态依靠其存储结构为[{},{}]来完成,每一个遍历结果都为对象,对象内部除结果外,还存在一个状态码用以区分
//allSettled方法的存储结构:返回内容+状态
// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: 一个错误 }
// ]
  • 因此在遍历传入数组参数时,无需辨认状态,直接统一存储到数组中

  • 存储内容为对象:

    1. 调用onFulfilled参数时,对象具备status、value参数,对应状态与值

    2. 调用onRejected参数时,对象具备status、reason参数,对应状态与拒绝值

static allSettled(promises) {
  // 问题关键: 什么时候要执行resolve, 什么时候要执行reject
  return new MyPromise((resolve, reject) => {
    const values = []
    promises.forEach(promise => {
      promise.then(res => {
        values.push({ status: PROMISE_STATUS_FULFILLED, value: res })
        if (values.length === promises.length) resolve(values)
      }, err => {
        values.push({ status: PROMISE_STATUS_REJECTED, reason: err })
        if (values.length === promises.length) resolve(values)
      })
    })
  })
}

手写rece/any方法

  • rece方法接收数组参数,返回数组参数内调用最快的一个异步结果
    • 直接遍历调用,返回结果即可
    • 第一个调用resolve或reject的异步promise会直接终止循环
static race(promises) {
  return new HYPromise((resolve, reject) => {
    promises.forEach(promise => {
      // promise.then(res => {
      //   resolve(res)
      // }, err => {
      //   reject(err)
      // })
      //代码优化
      promise.then(resolve, reject)
    })
  })
}
  • any方法接收数组参数,返回数组参数内第一个调用成功的异步结果全部拒绝状态的结果
    • 因此调用成功直接返回
    • 调用失败需要接收所有失败结果汇总数组进行返回(与前面all方法一致的判断逻辑),多个错误返回采用AggregateError对象进行包裹返回,因为AggregateError对象可以用标准化的方式来处理多个错误,使错误被作为一个单一的异常来处理,而不是一系列分散的错误,有助于错误的统一管理和处理
    • 标准化方式的优势在于内置了各类优秀的处理模式,后续扩展方便,例如:更合适的数据结构保存、语义保持一致、继承Error对象所有特性(例如堆栈跟踪)

MDN文档对thenable的描述

图27-11 AggregateError对象说明

static any(promises) {
  // resolve必须等到有一个成功的结果
  // reject所有的都失败才执行reject
  const reasons = []
  return new HYPromise((resolve, reject) => {
    promises.forEach(promise => {
      promise.then(resolve, err => {
        reasons.push(err)
        if (reasons.length === promises.length) {
          reject(new AggregateError(reasons))
        }
      })
    })
  })
}
  • 手写Promise到这里就告一段落,我们来对以上手写的所有方法进行一个总结回顾
    • 每一个方法都会遇到什么问题,我们需要怎么做?对应的解决思路有什么
    • 跟随该表格27-2脉络回顾以上所学,打牢基础


表27-2 手写Promise关键点回顾

方法/功能描述遇到的困难解决思路
构造函数初始化状态、值和回调函数列表必须正确管理状态转换使用状态变量控制转换,确保状态只能从 pending 转为 fulfilled 或 rejected
then添加解决和拒绝的回调,返回一个新的 Promise1. 异步执行回调。 2. 链式调用。 3. 错误处理1. 使用 queueMicrotask 确保异步性。 2. 返回新的 Promise 以支持链式。 3. 引入 try...catch
catch添加拒绝回调必须正确传递错误将 catch 实现为 then 的特殊形式,只传入拒绝回调
finally添加总是会执行的回调,不论 Promise 的结果如何回调不应影响 Promise 结果的传递使用 then 将回调包装,确保值和状态的不变传递
resolve创建一个解决的 Promise直接返回一个新的已解决的 Promise
reject创建一个拒绝的 Promise直接返回一个新的已拒绝的 Promise
all当所有 Promise 都成功时解决,任何一个失败都失败管理多个 Promise 的结果,并正确处理第一个拒绝存储每个 Promise 结果,使用计数器追踪解决的数量,任何拒绝立即结束
allSettled等待所有 Promise 结束,无论结果如何管理并格式化所有结果使用格式化的对象数组来表示每个 Promise 的结果状态和值或拒绝原因
race返回最先解决或拒绝的 Promise 的结果快速传递第一个结果监听所有 Promise,第一个解决或拒绝的决定了结果
any只要有一个 Promise 解决就解决,所有都拒绝才拒绝管理拒绝,并在所有都拒绝时返回一个合成的错误使用 AggregateError 来收集和传递所有拒绝的原因
  • 完整代码如下,可自行运行测试:
// ES6 ES2015
// https://promisesaplus.com/
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

// 工具函数
function execFunctionWithCatchError(execFn, value, resolve, reject) {
  try {
    const result = execFn(value)
    resolve(result)
  } catch(err) {
    reject(err)
  }
}

class MyPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledFns = []
    this.onRejectedFns = []

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        // 添加微任务
        queueMicrotask(() => {
          if (this.status !== PROMISE_STATUS_PENDING) return
          this.status = PROMISE_STATUS_FULFILLED
          this.value = value
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)
          })
        });
      }
    }

    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        // 添加微任务
        queueMicrotask(() => {
          if (this.status !== PROMISE_STATUS_PENDING) return
          this.status = PROMISE_STATUS_REJECTED
          this.reason = reason
          this.onRejectedFns.forEach(fn => {
            fn(this.reason)
          })
        })
      }
    }

    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    const defaultOnRejected = err => { throw err }
    onRejected = onRejected || defaultOnRejected

    const defaultOnFulfilled = value => { return value }
    onFulfilled = onFulfilled || defaultOnFulfilled

    return new MyPromise((resolve, reject) => {
      // 1.如果在then调用的时候, 状态已经确定下来
      if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
        execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
      }
      if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
        execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
      }

      // 2.将成功回调和失败的回调放到数组中
      if (this.status === PROMISE_STATUS_PENDING) {
        if (onFulfilled) this.onFulfilledFns.push(() => {
          execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
        })
        if (onRejected) this.onRejectedFns.push(() => {
          execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
        })
      }
    })
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }

  finally(onFinally) {
    this.then(() => {
      onFinally()
    }, () => {
      onFinally()
    })
  }

  static resolve(value) {
    return new MyPromise((resolve) => resolve(value))
  }

  static reject(reason) {
    return new MyPromise((resolve, reject) => reject(reason))
  }

  static all(promises) {
    // 问题关键: 什么时候要执行resolve, 什么时候要执行reject
    return new MyPromise((resolve, reject) => {
      const values = []
      promises.forEach(promise => {
        promise.then(res => {
          values.push(res)
          if (values.length === promises.length) {
            resolve(values)
          }
        }, err => {
          reject(err)
        })
      })
    })
  }

  static allSettled(promises) {
    return new MyPromise((resolve) => {
      const results = []
      promises.forEach(promise => {
        promise.then(res => {
          results.push({ status: PROMISE_STATUS_FULFILLED, value: res})
          if (results.length === promises.length) {
            resolve(results)
          }
        }, err => {
          results.push({ status: PROMISE_STATUS_REJECTED, value: err})
          if (results.length === promises.length) {
            resolve(results)
          }
        })
      })
    })
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(promise => {
        promise.then(resolve, reject)
      })
    })
  } 

  static any(promises) {
    // resolve必须等到有一个成功的结果
    // reject所有的都失败才执行reject
    const reasons = []
    return new MyPromise((resolve, reject) => {
      promises.forEach(promise => {
        promise.then(resolve, err => {
          reasons.push(err)
          if (reasons.length === promises.length) {
            reject(new AggregateError(reasons))
          }
        })
      })
    })
  }
}

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => { reject(1111) }, 3000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => { reject(2222) }, 2000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => { reject(3333) }, 3000)
})


// MyPromise.race([p1, p2, p3]).then(res => {
//   console.log("res:", res)
// }).catch(err => {
//   console.log("err:", err)
// })

MyPromise.any([p1, p2, p3]).then(res => {
  console.log("res:", res)
}).catch(err => {
  console.log("err:", err.errors)
})

//module.exports = MyPromise 导出模块用于后续测试

Promise A+规范测试

在实现完我们的MyPromise之后,我们需要通过Promises/A+的测试来验证我们的实现是否正确。Promises/A+提供了一组测试用例,我们可以用这些测试用例来确保我们的HYPromise满足Promises/A+规范

安装Promises/A+测试库

首先,我们需要安装Promises/A+的测试库。在项目目录下运行以下命令:

  • 这将在项目中安装promises-aplus-tests
//创建一个 package.json 文件
npm init
//安装 promises-aplus-tests 包  --save-dev 指定这个包仅用于开发环境
npm install --save-dev promises-aplus-tests

编写测试适配器

接下来,我们需要编写一个适配器文件,以便promises-aplus-tests库能够测试我们的MyPromise实现。在项目目录下创建一个名为adapter.js的文件,然后在其中添加以下代码:

const MyPromise = require('./MyPromise'); // 导入我们实现的MyPromise模块,同时MyPromise模块内记得导出

// 暴露适配器对象
module.exports = {
  resolved: MyPromise.resolve,
  rejected: MyPromise.reject,
  deferred() {
    const result = {};
    result.promise = new MyPromise((resolve, reject) => {
      result.resolve = resolve;
      result.reject = reject;
    });
    return result;
  }
};

这个适配器文件导出了一个对象,其中包含了resolvedrejecteddeferred方法。这些方法分别对应MyPromise的resolvereject方法和一个返回延迟对象(包含一个新的Promise实例以及对应的resolve和reject方法)的函数

运行测试

在项目目录下创建一个名为test.js的文件,然后在其中添加以下代码:

const promisesAplusTests = require('promises-aplus-tests');
const adapter = require('./adapter');

promisesAplusTests(adapter, function (err) {
  if (err) {
    console.error('Promises/A+ 测试失败:');
    console.error(err);
  } else {
    console.log('Promises/A+ 测试通过');
  }
});

这个文件导入了promises-aplus-tests库和我们编写的适配器。然后,我们调用promisesAplusTests函数,传入适配器对象和一个回调函数。如果测试通过,我们会在控制台输出“Promises/A+ 测试通过”,否则会输出错误信息。

最后,运行以下命令执行测试:

//node 测试文件.js
node test.js

如果我们的HYPromise实现正确,我们应该看到“Promises/A+ 测试通过”的输出。如果测试失败,我们需要根据错误信息修改我们的HYPromise实现(注意我们该系列实现只完成起主枝干,还有更多边界判定可以自行开拓),然后重新运行测试,直到所有测试都通过。

Promise A+规范的872个测试点通过示例图

图27-12 Promise A+规范的872个测试点通过示例图

后续预告

  • 在下一章节中,我们会学习迭代器与生成器,在for of等多个语句中我们常看见输入对象必须是一个可迭代对象,这和我们的迭代器与生成器有什么关联呢?
    • 在概念上涉及到了协程等不易理解的内容,我们又要如何学习?
    • 生成器函数使用 function* 语法和 yield 关键字,和普通的函数区别又在哪里?为什么我们平时编写代码时,很少见过这种写法?但这概念又至关重要,很难看到迭代器和生成器在实际开发中的应用导致我们对其一知半解
    • 因此,我们接下来会学习迭代器与生成器的各种写法,并区分清楚可迭代协议与迭代器协议的区别
  • 更会通过多种方式,一步步优化如何实现的遍历操作,例如遍历不可遍历的对象,是不是见过这个很矛盾的需求?
  • PS:在手写Promise方法时,对于try..catch的使用较为高频,为此我们封装了一个方法来实现复用性,目前有一个进入阶段4即将发布的特性能够弥补这一点,不妨让我们来稍微了解下,如图27-13
    • GitHub地址:github.com/tc39/propos…
    • 这个提案用于简化同步和异步函数的统一处理。它将任意函数包装在一个 Promise 中,确保函数在当前调用栈中执行,并返回一个 Promise,处理可能的返回值或异常

stack overflow社区关于浏览器中对象排序的讨论

图27-13 Promise.try提案阶段四