关于前端:Promise拆解计划手写Promise并通过官方全部测试用例

90次阅读

共计 6717 个字符,预计需要花费 17 分钟才能阅读完成。

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

前言

本系列上期带着大家一起拆解了 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 的毒打~

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

咱们下期见!

正文完
 0