从现在开始,我们就来动手实现一个功能完整的Promise,一步步深挖其中的细节。我们先从链式调用开始。

简易版实现

首先写出第一版的代码:

  1. //定义三种状态
  2. const PENDING = "pending";
  3. const FULFILLED = "fulfilled";
  4. const REJECTED = "rejected";
  5. function MyPromise(executor) {
  6. let self = this; // 缓存当前promise实例
  7. self.value = null;
  8. self.error = null;
  9. self.status = PENDING;
  10. self.onFulfilled = null; //成功的回调函数
  11. self.onRejected = null; //失败的回调函数
  12. const resolve = (value) => {
  13. if(self.status !== PENDING) return;
  14. setTimeout(() => {
  15. self.status = FULFILLED;
  16. self.value = value;
  17. self.onFulfilled(self.value);//resolve时执行成功回调
  18. });
  19. };
  20. const reject = (error) => {
  21. if(self.status !== PENDING) return;
  22. setTimeout(() => {
  23. self.status = REJECTED;
  24. self.error = error;
  25. self.onRejected(self.error);//resolve时执行成功回调
  26. });
  27. };
  28. executor(resolve, reject);
  29. }
  30. MyPromise.prototype.then = function(onFulfilled, onRejected) {
  31. if (this.status === PENDING) {
  32. this.onFulfilled = onFulfilled;
  33. this.onRejected = onRejected;
  34. } else if (this.status === FULFILLED) {
  35. //如果状态是fulfilled,直接执行成功回调,并将成功值传入
  36. onFulfilled(this.value)
  37. } else {
  38. //如果状态是rejected,直接执行失败回调,并将失败原因传入
  39. onRejected(this.error)
  40. }
  41. return this;
  42. }

可以看到,Promise 的本质是一个有限状态机,存在三种状态:

  • PENDING(等待)
  • FULFILLED(成功)
  • REJECTED(失败)

状态改变规则如下图:

对于 Promise 而言,状态的改变不可逆,即由等待态变为其他的状态后,就无法再改变了。

不过,回到目前这一版的 Promise, 还是存在一些问题的。

设置回调数组

首先只能执行一个回调函数,对于多个回调的绑定就无能为力,比如下面这样:

  1. let promise1 = new MyPromise((resolve, reject) => {
  2. fs.readFile('./001.txt', (err, data) => {
  3. if(!err){
  4. resolve(data);
  5. }else {
  6. reject(err);
  7. }
  8. })
  9. });
  10. let x1 = promise1.then(data => {
  11. console.log("第一次展示", data.toString());
  12. });
  13. let x2 = promise1.then(data => {
  14. console.log("第二次展示", data.toString());
  15. });
  16. let x3 = promise1.then(data => {
  17. console.log("第三次展示", data.toString());
  18. });

这里我绑定了三个回调,想要在 resolve() 之后一起执行,那怎么办呢?

需要将 onFulfilledonRejected 改为数组,调用 resolve 时将其中的方法拿出来一一执行即可。

  1. self.onFulfilledCallback = [];
  2. self.onRejectedCallback = [];
  1. MyPromise.prototype.then = function(onFulfilled, onRejected) {
  2. if (this.status === PENDING) {
  3. this.onFulfilledCallbacks.push(onFulfilled);
  4. this.onRejectedCallbacks.push(onRejected);
  5. } else if (this.status === FULFILLED) {
  6. onFulfilled(this.value);
  7. } else {
  8. onRejected(this.error);
  9. }
  10. return this;
  11. }

接下来将 resolve 和 reject 方法中执行回调的部分进行修改:

  1. // resolve 中
  2. self.onFulfilledCallback.forEach((callback) => callback(self.value));
  3. //reject 中
  4. self.onRejectedCallback.forEach((callback) => callback(self.error));

链式调用完成

我们采用目前的代码来进行测试:

  1. let fs = require('fs');
  2. let readFilePromise = (filename) => {
  3. return new MyPromise((resolve, reject) => {
  4. fs.readFile(filename, (err, data) => {
  5. if(!err){
  6. resolve(data);
  7. }else {
  8. reject(err);
  9. }
  10. })
  11. })
  12. }
  13. readFilePromise('./001.txt').then(data => {
  14. console.log(data.toString());
  15. return readFilePromise('./002.txt');
  16. }).then(data => {
  17. console.log(data.toString());
  18. })
  19. // 001.txt的内容
  20. // 001.txt的内容

咦?怎么打印了两个 001,第二次不是读的 002 文件吗?

问题出在这里:

  1. MyPromise.prototype.then = function(onFulfilled, onRejected) {
  2. //...
  3. return this;
  4. }

这么写每次返回的都是第一个 Promise。then 函数当中返回的第二个 Promise 直接被无视了!

说明 then 当中的实现还需要改进, 我们现在需要对 then 中返回值重视起来。

  1. MyPromise.prototype.then = function (onFulfilled, onRejected) {
  2. let bridgePromise;
  3. let self = this;
  4. if (self.status === PENDING) {
  5. return bridgePromise = new MyPromise((resolve, reject) => {
  6. self.onFulfilledCallbacks.push((value) => {
  7. try {
  8. // 看到了吗?要拿到 then 中回调返回的结果。
  9. let x = onFulfilled(value);
  10. resolve(x);
  11. } catch (e) {
  12. reject(e);
  13. }
  14. });
  15. self.onRejectedCallbacks.push((error) => {
  16. try {
  17. let x = onRejected(error);
  18. resolve(x);
  19. } catch (e) {
  20. reject(e);
  21. }
  22. });
  23. });
  24. }
  25. //...
  26. }

假若当前状态为 PENDING,将回调数组中添加如上的函数,当 Promise 状态变化后,会遍历相应回调数组并执行回调。

但是这段程度还是存在一些问题:

  1. 首先 then 中的两个参数不传的情况并没有处理,
  2. 假如 then 中的回调执行后返回的结果(也就是上面的x)是一个 Promise, 直接给 resolve 了,这是我们不希望看到的。

怎么来解决这两个问题呢?

先对参数不传的情况做判断:

  1. // 成功回调不传给它一个默认函数
  2. onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
  3. // 对于失败回调直接抛错
  4. onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };

然后对返回Promise的情况进行处理:

  1. function resolvePromise(bridgePromise, x, resolve, reject) {
  2. //如果x是一个promise
  3. if (x instanceof MyPromise) {
  4. // 拆解这个 promise ,直到返回值不为 promise 为止
  5. if (x.status === PENDING) {
  6. x.then(y => {
  7. resolvePromise(bridgePromise, y, resolve, reject);
  8. }, error => {
  9. reject(error);
  10. });
  11. } else {
  12. x.then(resolve, reject);
  13. }
  14. } else {
  15. // 非 Promise 的话直接 resolve 即可
  16. resolve(x);
  17. }
  18. }

然后在 then 的方法实现中作如下修改:

  1. resolve(x) -> resolvePromise(bridgePromise, x, resolve, reject);

在这里大家好好体会一下拆解 Promise 的过程,其实不难理解,我要强调的是其中的递归调用始终传入的resolvereject这两个参数是什么含义,其实他们控制的是最开始传入的bridgePromise的状态,这一点非常重要。

紧接着,我们实现一下当 Promise 状态不为 PENDING 时的逻辑。

成功状态下调用then:

  1. if (self.status === FULFILLED) {
  2. return bridgePromise = new MyPromise((resolve, reject) => {
  3. try {
  4. // 状态变为成功,会有相应的 self.value
  5. let x = onFulfilled(self.value);
  6. // 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程
  7. resolvePromise(bridgePromise, x, resolve, reject);
  8. } catch (e) {
  9. reject(e);
  10. }
  11. })
  12. }

失败状态下调用then:

  1. if (self.status === REJECTED) {
  2. return bridgePromise = new MyPromise((resolve, reject) => {
  3. try {
  4. // 状态变为失败,会有相应的 self.error
  5. let x = onRejected(self.error);
  6. resolvePromise(bridgePromise, x, resolve, reject);
  7. } catch (e) {
  8. reject(e);
  9. }
  10. });
  11. }

Promise A+中规定成功和失败的回调都是微任务,由于浏览器中 JS 触碰不到底层微任务的分配,可以直接拿 setTimeout(属于宏任务的范畴) 来模拟,用 setTimeout将需要执行的任务包裹 ,当然,上面的 resolve 实现也是同理, 大家注意一下即可,其实并不是真正的微任务。

  1. if (self.status === FULFILLED) {
  2. return bridgePromise = new MyPromise((resolve, reject) => {
  3. setTimeout(() => {
  4. //...
  5. })
  6. }
  1. if (self.status === REJECTED) {
  2. return bridgePromise = new MyPromise((resolve, reject) => {
  3. setTimeout(() => {
  4. //...
  5. })
  6. }

好了,到这里, 我们基本实现了 then 方法,现在我们拿刚刚的测试代码做一下测试, 依次打印如下:

  1. 001.txt的内容
  2. 002.txt的内容

可以看到,已经可以顺利地完成链式调用。

错误捕获及冒泡机制分析

现在来实现 catch 方法:

  1. Promise.prototype.catch = function (onRejected) {
  2. return this.then(null, onRejected);
  3. }

对,就是这么几行,catch 原本就是 then 方法的语法糖。

相比于实现来讲,更重要的是理解其中错误冒泡的机制,即中途一旦发生错误,可以在最后用 catch 捕获错误。

我们回顾一下 Promise 的运作流程也不难理解,贴上一行关键的代码:

  1. // then 的实现中
  2. onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };

一旦其中有一个PENDING状态的 Promise 出现错误后状态必然会变为失败, 然后执行 onRejected函数,而这个 onRejected 执行又会抛错,把新的 Promise 状态变为失败,新的 Promise 状态变为失败后又会执行onRejected……就这样一直抛下去,直到用catch 捕获到这个错误,才停止往下抛。

这就是 Promise 的错误冒泡机制

至此,Promise 三大法宝: 回调函数延迟绑定回调返回值穿透错误冒泡

参考文章

三元博客