大家好,欢送来到前端研习圈的今日分享。

前言

本系列上期带着大家一起拆解了 Promises/A+ 标准。从概念,术语,束缚条例等方面理解了标准

那么本期咱们要做的就是从标准到实现,并且通过官网的所有测试用例。为了和原生的 Promise 有所区别,咱们就把这一版实现命名为 _Promise。齐全状态曾经上传到 github,须要的同学自取

提醒:_Promise 仅关注具体实现,不关注成员办法具体应该是公有还是私有等设计细节

源码地址 ->
https://github.com/Mumujianguang/_promise

接下来咱们就进入主题,首先咱们大略整顿一下 todo list

  • 定义 Promise 的状态枚举
  • 定义 Promise 的类构造
  • 实现构造函数
  • 实现 then 办法

结构设计

首先,咱们先定义一个枚举对象,将 Promise 的三种状态定义进去

const PromiseState = {    pending: 'pending',    fulfilled: 'fulfilled',    rejected: 'rejected'}

而后简略设计一下类构造,同时将 state 初始化为 pending 状态

class _Promise {    state = PromiseState.pending;    value;       reason;    fulfilledQueue = [];    rejectedQueue = [];    constructor(executor) {}    resolve(value) {}    reject(reason) {}    then(onFulfilled, onRejected) {}}

因为 then 能够调用屡次,所以咱们设计 fulfilledQueuerejectedQueue 两个数组来别离存储 then 所注册的 胜利的回调失败的回调

为了后续不便外部调用,这里将 resolvereject 两个办法也间接定义在类上

实现

有了根底构造,那么咱们就能够开始着手实现各个办法了。先从 constructor 开始

constructor

回顾一下 Promise 的用法,为了不便解说,咱们把在结构 Promise 实例时传入的函数独自提出来,它的学名叫 executor,接管 resolvereject 两个参数

const executor = (resolve, reject) => {}const p = new Promise(executor)

看到这儿,置信大家应该都有 constructor 的实现思路了。

但须要留神一点,为了避免 executor 执行时外部报错,须要 try catch 解决一下,并且在 catch 的场景间接将 Promise reject

constructor(executor) {    try {        executor(            (value) => this.resolve(value),            (reason) => this.reject(reason)        )    } catch(e) {        this.reject(e);    }}

以上就是 constructor 的实现逻辑,接下来咱们顺着这个思路持续

resolve & reject

在执行 executor 时,咱们将 resolvereject 作为参数传了进去,咱们一起回顾一下它们的作用是什么

  • 接管一个 value/reason
  • Promise 的状态批改为胜利/失败
  • value/reason 作为 胜利/失败 回调的参数并依照注册的程序批量执行
  • 状态一旦扭转就不能再被调用

性能点很清晰,那么咱们就能够一条一条去实现它们,间接看代码吧

resolve(value) {    if (this.state !== PromiseState.pending) {        return;    }    this.state = PromiseState.fulfilled;    this.value = value;    this.fulfilledQueue.forEach((onFulfilled) => onFulfilled(value))}
reject(reason) {    if (this.state !== PromiseState.pending) {        return;    }    this.state = PromiseState.rejected;    this.reason = reason;    this.rejectedQueue.forEach((onRejected) => onRejected(reason))}

到这里,咱们就实现了 resolve & reject 的实现了,还是比较简单对不对,那么接下来要上强度咯

then

首先咱们思考一下 then 的作用是什么,再同步回顾一下用法

const p = new Promise(resolve => resolve('done'))p.then(  value => console.log(value),  reason => console.log(reason),)

then 的性能其实很简略,就是单纯注册 胜利/失败 的回调,但就是看似如此简略的办法,它的实现难度却是整个 Promise 中最高的。

但不要慌,咱们明天的指标就是要搞懂它,翻过那座山(背地还是山)!

联合上期标准中所讲,咱们先简略概括两点

  • then 返回的是一个新的Promise
  • 接管 onFulfilledonRejected 作为参数

先写出如下代码

then(onFulfilled, onRejected) {    const p2 = new _Promise((resolve, reject) => {      // TODO    })    return p2;}

当初 then 的整体框架有了,咱们持续思考,因为 onFulfilledonRejected 的返回值会决定 p2 的状态,那么在注册之前,咱们必定须要对这两个办法做一层包装,将新的Promise的 resolve 和 reject 的执行权onFulfilled/onRejected 的返回值关联起来,由返回值的具体情况决定

也就是说 then 的其余逻辑咱们须要写在 新Promiseexecutor 中。同时还须要留神的是,在 executor 中咱们就须要拜访 p2,但 p2 是在 executor 执行结束之后才被赋的值,间接拜访必定会报错

then(onFulfilled, onRejected) {    const p2 = new _Promise((resolve, reject) => {        console.log(p2) // Uncaught ReferenceError: Cannot access 'p2' before initialization    })    return p2;}
Uncaught ReferenceError: Cannot access 'p2' before initialization

如上代码所示,咱们会失去一个与预期统一的报错,怎么躲避呢?其实很容易解决,放到异步工作外面去执行不就好了吗,这里咱们用 queueMicrotask 来实现

then(onFulfilled, onRejected) {  const p2 = new _Promise((resolve, reject) => {    queueMicrotask(() => {      console.log(p2) // _Promise {}    })  })  return p2;}

搞定,当初就能失常拜访 p2

那么接下咱们持续依照标准的束缚条例给 then 的实现添砖加瓦,先梳理一下大抵要做的事件

  • onFulfilledonRejected 增加兼容逻辑(参考条例 2.2.1 & 2.2.7.3 & 2.2.7.4
  • 包装 onFulfilledonRejected,它们的返回值将决定 p2 的 resolve 和 reject 如何执行。因而这里咱们再形象一层 包装器 函数(wrapCallback)进去,由这个函数来返回包装后的 onFulfilledresolveCallback) 和 onRejectedrejectCallback
  • 判断以后 Promise 的状态是否曾经确定,是的话则间接调用resolveCallbackrejectCallback,否则将它们注册到各自的回调队列中

梳理结束,就敲代码实现吧

then(onFulfilled, onRejected) {  const p2 = new _Promise((resolve, reject) => {    queueMicrotask(() => {      onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => resolve(this.value);      onRejected = typeof onRejected === 'function' ? onRejected : () => reject(this.reason);      const resolveCallback = this.wrapCallback(        p2,        onFulfilled,        resolve,        reject      );      const rejectCallback = this.wrapCallback(        p2,        onRejected,        resolve,        reject      );      if (this.state === PromiseState.fulfilled) {          resolveCallback(this.value);          return;      }      if (this.state === PromiseState.rejected) {          rejectCallback(this.reason);          return;      }      this.fulfilledQueue.push(          resolveCallback      )      this.rejectedQueue.push(          rejectCallback      )    })  })  return p2;}

至此 then 办法咱们就曾经实现啦,完结撒花!最难的局部也不过如此...

等等,不太对劲,then 是实现完了,但咱们刚刚又引入了一层包装器 还没实现呢,还得持续呀各位~

顺便揭示一下大家,还记得上期咱们留的一个大坑吗,没错就是标准中的束缚条例2.3 所形容的 Promise Resolution Procedure 流程,但咱们当初不是只剩下 wrapCallback 了吗,所以这个解决流程只能在 wrapCallback 中实现了,为了和标准保持一致,咱们把 Promise Resolution Procedure 流程独自用一个办法来实现,就取名叫 resolutionProcedure

这样一来 wrapCallback 的实现就异样简略了,但留神 then 的回调须要放在 micro task 中去执行,这里咱们还是用 queueMicrotask 来实现;同时还是须要思考回调外部执行报错的场景,所以加上 try catch 来捕捉异样,并在异样的case,触发 reject

基于下面的剖析,咱们就能敲出以下代码了

wrapCallback(promise2, callback, resolve, reject) {  return (arg) => queueMicrotask(() => {    let x;    try {        x = callback(arg);    } catch (error) {        reject(error);        return;    }    this.resolutionProcedure(promise2, x, resolve, reject)  })}

OK,当初 wrapCallback 实现结束!

终于,咱们来到了最初一个办法 resolutionProcedure,还记得这个流程是做什么的吗,先抛开其余细节,此流程次要是为了解决 x 是一个 thenable 的场景,以反对第三方实现的 类promise,满足互操作性的要求。

好吧,纠正一下我之前的措辞,在整个 Promise 实现中 resolutionProcedure 才是最难的(手动狗头)

那么接下来咱们还是依据标准 条例2.3 来梳理出须要实现的逻辑点

  1. 因为这外面存在自调用,所以当 p2x 是同一个对象时,为了避免死循环,须要间接退出后续解决并触发 reject
  2. x 是一个 Promise 时,则间接将 resolve & reject 包装后注册到 x 上,由 x 的最终状态来决定 p2 的状态
  3. x 是一个一般对象或者办法时,如果 x 存在 then 办法,则将其视为 thenable。留神,这里须要思考 then 是一个 getter 的状况,也就是意味着在拜访 x.then 时也可能会报错,因而获取 then 的过程也须要 try catch 包裹一下,报错的状况间接触发 reject。后续解决的指标和第2点统一,还是遵循 resolvereject 只能触发一次的准则,需思考 then 执行报错的场景,这里就不做赘述了
  4. 以上条件均不满足时,则将 x 作为 value 触发 resolve

接下来就是编码工夫~

resolutionProcedure(promise2, x, resolve, reject) {    if (promise2 === x) {        reject(new TypeError('promise2 === x'));        return;    }    if (x instanceof _Promise) {        x.then(            (value) => this.resolutionProcedure(promise2, value, resolve, reject),            (reason) => reject(reason)        );        return;    }    if (        x !== null &&        typeof x === 'object' ||        typeof x === 'function'    ) {        let then;        try {            then = x.then;        } catch (error) {            reject(error);            return;        }        if (typeof then === 'function') {            let isCalledResolvePromise = false;            let isCalledRejectPromise = false;            const resolvePromise = (value) => {                if (isCalledResolvePromise || isCalledResolvePromise) {                    return;                }                isCalledResolvePromise = true;                this.resolutionProcedure(promise2, value, resolve, reject)            }            const rejectPromise = (reason) => {                if (isCalledRejectPromise || isCalledResolvePromise) {                    return;                }                isCalledRejectPromise = true;                reject(reason)            }            try {                then.call(                    x,                    resolvePromise,                    rejectPromise                )            } catch (error) {                if (                    !isCalledRejectPromise &&                    !isCalledResolvePromise                ) {                    reject(error)                }            }            return;        }    }    resolve(x);}

至此,咱们的 _Promise 就全副编码结束,那它是否像原生的 Promise 失常工作呢,连忙去测试一下吧!

当然也不必大家去手写测试用例了,官网提供了一个 npmpromises-aplus-tests

github地址 -> https://github.com/promises-aplus/promises-tests

依据官网的文档形容,要执行测试用例咱们还须要导出一个规范构造

那么咱们就依照要求导出如下对象

module.exports = {    resolved(value) {        return new _Promise((resolve) => resolve(value))    },    rejected(reason) {        return new _Promise((_, reject) => reject(reason))    },    deferred() {        const ret = {};        ret.promise = new _Promise((resolve, reject) => {            ret.resolve = resolve;            ret.reject = reject        })        return ret;    }}

值得一提的是,deferred 是不是与 Promise.withResolvers 的性能一模一样

回到正题,装置promises-aplus-tests后,咱们依据官网文档的测试指南配置一下测试指令

// package.json..."scripts": {  "test": "promises-aplus-tests ./core/Promise.cjs"},...

接下来就能够测试了,在控制台输出

pnpm run test

OK,872个用例全副通过

写在最初

到这里 Promise拆解打算 终于实现,这个系列的阶段性指标也达成了,心愿这个系列能真正帮忙到大家,从此不再受 Promise 的毒打~

那么这期就到这里,如果感觉有用的话记得点赞加关注哦!

咱们下期见!