实现一个满足 A+ 标准的 Promise

本来我对这种八股是不屑一顾的,什么手写 Promise 手写 DeepEqual 之类的我都不屑一顾。

直到我真的笔试遇到了这种题。

因为没有实现过,笔试的时候也没有足够的时间捋清楚逻辑,所以基本上测试用例只过了几个最标准的,复杂一点的一个都没过。本来觉得这种东西没什么好研究的,后来发现这个东西有专门的测试集,这就有趣起来了,毕竟我还是挺喜欢那种测试用例全通过的感觉的。

幸好这个问题已经有不少人研究过了,有很多现成的源码可以参考。这篇主要记录一下我看代码时的困惑和难点记录。

本 Promise 实现源码参考了 https://jiawei397.github.io/docs/note/js/es6/promisePolyfill.html

如何测试 Promise 实现是否符合标准

https://github.com/promises-aplus/promises-tests

首先 npm 安装一下 promises-aplus-tests ,然后 package.jsonscript 加上

"scripts": {
    "test": "run-my-own-tests && promises-aplus-tests test/my-adapter"
}

然后在 MyPromise 类里加上这个方法:

static deferred() {
    let result = {};
    result.promise = new Promise((resolve, reject) => {
      result.resolve = resolve;
      result.reject = reject;
    });
    return result;
}

module.exports 导出 MyPromise:

module.exports = MyPromise;

npm run test 就可以测试了。

Promise A+ 标准

https://promisesaplus.com/

这是 Promise A+ 的标准。其中包括三个部分。

第一部分

人话:

一个 Promise 有三种状态,pending,fulfuilled,rejected,之间的转换关系如图:

只能由 pending 转为其他两个状态,其余的转换都不允许。

这个特性实现比较简单,只要在 resolve 或者 reject 开头检查一下是不是 pending,如果不是的话直接 return 就可以了。

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  status = PENDING;

  constructor(fn) {
    const resolve = (value) => {
      if (this.status !== PENDING) return;
      this.status = FULFILLED;
      this.value = value;
    };
    const reject = (err) => {
      if (this.status !== PENDING) return;
      this.status = REJECTED;
      this.reason = err;
    }

    try {
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
}

 

第二部分

第二部分主要描述 then() 函数的实现。这是 Promise 实现最核心的内容。

是不是觉得规范太多看得眼睛都花了?没关系,我们可以一个一个来满足。

首先看 2.2.7,then() 返回一个 promise,所以我们先

then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      
    })
 }

实现基本 then

2.2.2 和 2.2.3 基本除了状态不同,要求都是一样的,都是要求当前 promise 状态该为 fulfilled 或者 rejected 后,再去执行 onFulfilled 或者 onRejected,而且 onFulfilled, onRejected 都只能执行一次。

如果当前 promise 已经是 fulfilled 或者 rejected,那只要直接执行就好了,但是如果当前 promise 是 pending 该怎么办呢?或者说,要怎么保证当前 promise 状态改变后才执行 onFulfilled, onRejected 呢?

可以把 onFulfilled 和 onRejected 缓存起来,等到当前 promise 的 resolve 或 reject 被调用后再去调用 onFulfilled 或 onRejected。

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  status = PENDING;
  // 缓存起来
  onFulfilled = undefined;
  onRejected = undefined;

  constructor(fn) {
    const resolve = (value) => {
      // ...
      this.onFulfilled(this.value);
    };
    const reject = (err) => {
      // ...
      this.onRejected(this.reason);
    }

    try {
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        onFulfilled(this.value);
      } else if (this.status === REJECTED) {
        onRejected(this.reason);
      } else {
        this.onFulfilled = onFulfilled;
        this.onRejected = onRejected;
      }
    })
  }
}
new MyPromise((resolve, reject) => {
  resolve(2333);
}).then(console.log);


new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(999);
  }, 100)
}).then(console.log);

new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject('Not Legal');
  }, 90)
}).then(null, console.log);

这三个简单的测试用例都可以通过,到这里,我们就完成了基本情况。

此时 2.2.1,2.2.2,2.2.3,2.2.5 和 2.2.7(不包括子项)都已经满足了。

onFulfilled 或 onRejected 的特殊情况

多次调用 then

首先看 2.2.6,同一个 promise 可能调用多次 then,此时 then 的内容需要依次执行。

比如说:

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(2333);
  }, 100);
})
p.then(console.log);
p.then(console.warn);
p.then((val) => console.log(val + 1));

目前我们的代码只会输出 2334,这明显不符合 2.2.6。

这个问题解决起来也简单,只需要用两个列表来存所有的 onFulfilled 和 onRejected 就行。

class MyPromise {
  status = PENDING;
  // 缓存起来
  onFulfilledList = [];
  onRejectedList = [];

  constructor(fn) {
    const resolve = (value) => {
      // ...
      this.onFulfilledList.forEach(cb => cb(value));
      this.onFulfilledList = [];
    };
    const reject = (err) => {
      // ...
      this.onRejectedList.forEach(cb => cb(err));
      this.onRejectedList = [];
    }
    // ...
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        onFulfilled(this.value);
      } else if (this.status === REJECTED) {
        onRejected(this.reason);
      } else {
        this.onFulfilledList.push(onFulfilled);
        this.onRejectedList.push(onRejected);
      }
    })
  }
}

这样,刚才的测试代码输出就符合预期了,2.2.6 也满足了。

onFulfilled 或 onRejected 不为函数

2.2.7.3 和 2.2.7.4 要求 onFulfilled 和 onRejected 不为函数时,直接透传当前 promise 内的值。这个实现起来也简单。

作者: 梁小顺

脑子不太好用的普通人。 顺带一提性格也有点古怪。 在老妈子和厌世肥宅中来回切换。

《实现一个满足 A+ 标准的 Promise》有一个想法

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据