30分钟,带你实现一个符合规范的 Promise(巨详细)

2020/5/6 11:26:44

本文主要是介绍30分钟,带你实现一个符合规范的 Promise(巨详细),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

关于 Promise 原理解析的优秀文章,在掘金上已经有非常多了。但是笔者总是处在 看了就会,一写就废 的状态,这是笔者写这篇文章的目的,为了理一下 Promise 的编写思路,从零开始手写一波代码,同时也方便自己日后回顾。

 

Promise 的作用

PromiseJavaScript 异步编程的一种流行解决方案,它的出现是为了解决 回调地狱 的问题,让使用者可以通过链式的写法去编写写异步代码,具体的用法笔者就不介绍了,大家可以参考阮一峰老师的 ES6 Promise教程。

 

课前知识

观察者模式

什么是观察者模式:

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

Promise 是基于 观察者的设计模式 实现的,then 函数要执行的函数会被塞入观察者数组中,当 Promise 状态变化的时候,就去执行观察组数组中的所有函数。

事件循环机制

实现 Promise 涉及到了 JavaScript 中的事件循环机制 EventLoop、以及宏任务和微任务的概念。

事件循环机制的流程图如下:

大家可以看一下这段代码:

console.log(1);

setTimeout(() => {
  console.log(2);
},0);

let a = new Promise((resolve) => {
  console.log(3);
  resolve();
}).then(() => {
  console.log(4);
}).then(() => {
  console.log(5);
});

console.log(6);
复制代码

如果不能一下子说出输出结果,建议大家可以先查阅一下 事件循环 的相关资料,在掘金中有很多优秀的文章。

Promises/A+ 规范

Promises/A+ 是一个社区规范,如果你想写出一个规范的 Promise,我们就需要遵循这个标准。之后我们也会根据规范来完善我们自己编写的 Promise

 

Promise 核心知识点

在动手写 Promise 之前,我们先过一下几个重要的知识点。

executor

// 创建 Promise 对象 x1
// 并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
  // 业务逻辑处理成功结果
  const value = ...;
  resolve(value);
  // 失败结果
  // const reason = ...;
  // reject(reason);
}

let x1 = new Promise(executor);
复制代码

首先 Promise 是一个类,它接收一个执行函数 executor,它接收两个参数:resolvereject,这两个参数是 Promise 内部定义的两个函数,用来改变状态并执行对应回调函数。

因为 Promise 本身是不知道执行结果失败或者成功,它只是给异步操作提供了一个容器,实际上的控制权在使用者的手上,使用者可以调用上面两个参数告诉 Promise 结果是否成功,同时将业务逻辑处理结果(value/reason)作为参数传给 resolvereject 两个函数,执行回调。

三个状态

Promise 有三个状态:

  • pending:等待中
  • resolved:已成功
  • rejected:已失败

Promise 的状态改变只有两种可能:从 pending 变为 resolved 或者从 pending 变为 rejected,如下图(引自 Promise 迷你书):

引自 Promise 迷你书

而且需要注意的是一旦状态改变,状态不会再变了,接下来就一直是这个结果。也就是说当我们在 executor 函数中调用了 resolve 之后,之后调用 reject 就没有效果了,反之亦然。

// 并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
  resolve(100);
  // 之后调用 resolve,reject 都是无效的,
  // 因为状态已经变为 resolved,不会再改变了
  reject(100);
}

let x1 = new Promise(executor);
复制代码

then

每一个 promise 都一个 then 方法,这个是当 promise 返回结果之后,需要执行的回调函数,他有两个可选参数:

  • onFulfilled:成功的回调;
  • onRejected:失败的回调;

如下图(引自 Promise 迷你书):

引自 Promise 迷你书

// ...
let x1 = new Promise(executor);

// x1 延迟绑定回调函数 onResolve
function onResolved(value){
  console.log(value);
}

// x1 延迟绑定回调函数 onRejected
function onRejected(reason){
  console.log(reason);
}

x1.then(onResolved, onRejected);
复制代码

 

手写 Promise 大致流程

在这里我们简单过一下手写一个 Promise 的大致流程:

executor 与三个状态

  • new Promise 时,需要传递一个 executor 执行器函数,在构造函数中,执行器函数立刻执行
  • executor 执行函数接受两个参数,分别是 resolvereject
  • Promise 只能从 pendingrejected, 或者从 pendingfulfilled
  • Promise 的状态一旦确认,状态就凝固了,不在改变

then 方法

  • 所有的 Promise 都有 then 方法,then 接收两个参数,分别是 Promise 成功的回调 onFulfilled,和失败的回调 onRejected
  • 如果调用 then 时,Promise 已经成功,则执行 onFulfilled,并将 Promise 的值作为参数传递进去;如果 Promise 已经失败,那么执行 onRejected,并将 Promise 失败的原因作为参数传递进去;如果 Promise 的状态是 pending,需要将 onFulfilledonRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行(观察者模式)
  • then 的参数 onFulfilledonRejected 可以不传,Promise 可以进行值穿透

链式调用并处理 then 返回值

  • Promise 可以 then 多次,Promisethen 方法返回一个新的 Promise
  • 如果 then 返回的是一个正常值,那么就会把这个结果(value)作为参数,传递给下一个 then 的成功的回调(onFulfilled
  • 如果 then 中抛出了异常,那么就会把这个异常(reason)作为参数,传递给下一个 then 的失败的回调(onRejected)
  • 如果 then 返回的是一个 promise 或者其他 thenable 对象,那么需要等这个 promise 执行完撑,promise 如果成功,就走下一个 then 的成功回调;如果失败,就走下一个 then 的失败回调。

上面是大致的实现流程,如果迷迷糊糊没关系,只要大致有一个印象即可,后续我们会一一讲到。

那接下来我们就开始实现一个最简单的例子开始讲解。

 

第一版(从一个简单例子开始)

我们先写一个简单版,这版暂不支持状态、链式调用,并且只支持调用一个 then 方法。

来个 🌰

let p1 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      resolved('成功了');
    }, 1000);
})

p1.then((data) => {
    console.log(data);
}, (err) => {
    console.log(err);
})
复制代码

例子很简单,就是 1s 之后返回 成功了,并在 then 中输出。

实现

我们定义一个 MyPromise 类,接着我们在其中编写代码,具体代码如下:

class MyPromise {
  // ts 接口定义 ...
  constructor (executor: executor) {
    // 用于保存 resolve 的值
    this.value = null;
    // 用于保存 reject 的值
    this.reason = null;
    // 用于保存 then 的成功回调
    this.onFulfilled = null;
    // 用于保存 then 的失败回调
    this.onRejected = null;

    // executor 的 resolve 参数
    // 用于改变状态 并执行 then 中的成功回调
    let resolve = value => {
      this.value = value;
      this.onFulfilled && this.onFulfilled(this.value);
    }

    // executor 的 reject 参数
    // 用于改变状态 并执行 then 中的失败回调
    let reject = reason => {
      this.reason = reason;
      this.onRejected && this.onRejected(this.reason);
    }

    // 执行 executor 函数
    // 将我们上面定义的两个函数作为参数 传入
    // 有可能在 执行 executor 函数的时候会出错,所以需要 try catch 一下 
    try {
      executor(resolve, reject);
    } catch(err) {
      reject(err);
    }
  }

  // 定义 then 函数
  // 并且将 then 中的参数复制给 this.onFulfilled 和 this.onRejected
  private then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }
}
复制代码

好了,我们的第一版就完成了,是不是很简单。

不过这里需要注意的是,resolve 函数的执行时机需要在 then 方法将回调函数注册了之后,在 resolve 之后在去往赋值回调函数,其实已经完了,没有任何意义。

上面的例子没有问题,是因为 resolve(成功了) 是包在 setTimeout 中的,他会在下一个宏任务执行,这时回调函数已经注册了。

大家可以试试把 resolve(成功了)setTimeout 中拿出来,这个时候就会出现问题了。

存在问题

这一版实现很简单,还存在几个问题:

  • 未引入状态的概念

未引入状态的概念,现在状态可以随意变,不符合 Promise 状态只能从等待态变化的规则。

  • 不支持链式调用

正常情况下我们可以对 Promise 进行链式调用:

let p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolved('成功了');
  }, 1000);
})

p1.then(onResolved1, onRejected1).then(onResolved2, onRejected2)
复制代码
  • 只支持一个回调函数,如果存在多个回调函数的话,后面的会覆盖前面的

在这个例子中,onResolved2 会覆盖 onResolved1onRejected2 会覆盖 onRejected1

let p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolved('成功了');
  }, 1000);
})

// 注册多个回调函数
p1.then(onResolved1, onRejected1);
p1.then(onResolved2, onRejected2);
复制代码

接下来我们更进一步,把这些问题给解决掉。

 

第二版(实现链式调用)

这一版我们把状态的概念引入,同时实现链式调用的功能。

加上状态

上面我们说到 Promise 有三个状态:pendingresovledrejected,只能从 pending 转为 resovled 或者 rejected,而且当状态改变之后,状态就不能再改变了。

  • 我们定义一个属性 status:用于记录当前 Promise 的状态
  • 为了防止写错,我们把状态定义成常量 PENDINGRESOLVEDREJECTED
  • 同时我们将保存 then 的成功回调定义为一个数组:this.resolvedQueuesthis.rejectedQueues,我们可以把 then 中的回调函数都塞入对应的数组中,这样就能解决我们上面提到的第三个问题。
class MyPromise {
  private static PENDING = 'pending';
  private static RESOLVED = 'resolved';
  private static REJECTED = 'rejected';

  constructor (executor: executor) {
    this.status = MyPromise.PENDING;
    // ...

    // 用于保存 then 的成功回调数组
    this.resolvedQueues = [];
    // 用于保存 then 的失败回调数组
    this.rejectedQueues = [];

    let resolve = value => {
      // 当状态是 pending 是,将 promise 的状态改为成功态
      // 同时遍历执行 成功回调数组中的函数,将 value 传入
      if (this.status == MyPromise.PENDING) {
        this.value = value;
        this.status = MyPromise.RESOLVED;
        this.resolvedQueues.forEach(cb => cb(this.value))
      }
    }

    let reject = reason => {
      // 当状态是 pending 是,将 promise 的状态改为失败态
      // 同时遍历执行 失败回调数组中的函数,将 reason 传入
      if (this.status == MyPromise.PENDING) {
        this.reason = reason;
        this.status = MyPromise.REJECTED;
        this.rejectedQueues.forEach(cb => cb(this.reason))
      }
    }

    try {
      executor(resolve, reject);
    } catch(err) {
      reject(err);
    }
  }
}
复制代码

完善 then 函数

接着我们来完善 then 中的方法,之前我们是直接将 then 的两个参数 onFulfilledonRejected,直接赋值给了 Promise 的用于保存成功、失败函数回调的实例属性。

现在我们需要将这两个属性塞入到两个数组中去:resolvedQueuesrejectedQueues

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
    // 当参数不是函数类型时,需要创建一个函数赋值给对应的参数
    // 这也就实现了 透传
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}

    // 当状态是等待态的时候,需要将两个参数塞入到对应的回调数组中
    // 当状态改变之后,在执行回调函数中的函数
    if (this.status === MyPromise.PENDING) {
      this.resolvedQueues.push(onFulfilled)
      this.rejectedQueues.push(onRejected)
    }

    // 状态是成功态,直接就调用 onFulfilled 函数
    if (this.status === MyPromise.RESOLVED) {
      onFulfilled(this.value)
    }

    // 状态是成功态,直接就调用 onRejected 函数
    if (this.status === MyPromise.REJECTED) {
      onRejected(this.reason)
    }
  }
}
复制代码

then 函数的一些说明

  • 什么情况下 this.status 会是 pending 状态,什么情况下会是 resolved 状态

这个其实也和事件循环机制有关,如下代码:

// this.status 为 pending 状态
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 0)
}).then(value => {
  console.log(value)
})

// this.status 为 resolved 状态
new MyPromise((resolve, reject) => {
  resolve(1)
}).then(value => {
  console.log(value)
})
复制代码
  • 什么是 透传

如下面代码,当 then 中没有传任何参数的时候,Promise 会使用内部默认的定义的方法,将结果传递给下一个 then

let p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolved('成功了');
  }, 1000);
})

p1.then().then((res) => {
  console.log(res);
})
复制代码

因为我们现在还没支持链式调用,这段代码运行会出问题。

支持链式调用

支持链式调用,其实很简单,我们只需要给 then 函数最后返回 this 就行,这样就支持了链式调用:

class MyPromise {
  // ...
  private then(onFulfilled, onRejected) {
    // ...
    return this;
  }
}
复制代码

每次调用 then 之后,我们都返回当前的这个 Promise 对象,因为 Promise 对象上是存在 then 方法的,这个时候我们就简单的实现了 Promise 的简单调用。

这个时候运行上面 透传 的测试代码了。

但是上面的代码还是存在相应的问题的,看下面代码:

const p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');  
});

p1.then((res) => {
  console.log(res);
  return 'then1';
})
.then((res) => {
  console.log(res);
  return 'then2';
})
.then((res) => {
  console.log(res);
  return 'then3';
})

// 预测输出:resolved -> then1 -> then2
// 实际输出:resolved -> resolved -> resolved
复制代码

输出与我们的预期有偏差,因为我们 then 中返回的 this 代表了 p1,在 new MyPromise 之后,其实状态已经从 pending 态变为了 resolved 态,之后不会再变了,所以在 MyPromise 中的 this.value 值就一直是 resolved

这个时候我们就得看看关于 then 返回值的相关知识点了。

then 返回值

实际上 then 都会返回了一个新的 Promise 对象。

先看下面这段代码:

// 新创建一个 promise
const aPromise = new Promise(function (resolve) {
  resolve(100);
});

// then 返回的 promise
var thenPromise = aPromise.then(function (value) {
  console.log(value);
});

console.log(aPromise !== thenPromise); // => true
复制代码

从上面的代码中我们可以得出 then 方法返回的 Promise 已经不再是最初的 Promise 了,如下图(引自 Promise 迷你书):

引自 Promise 迷你书

promise 的链式调用跟 jQuery 的链式调用是有区别的,jQuery 链式调用返回的对象还是最初那个 jQuery 对象;Promise 更类似于数组中一些方法,如 slice,每次进行操作之后,都会返回一个新的值。

改造代码

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason}

    // then 方法返回一个新的 promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 成功状态,直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 将 onFulfilled 函数的返回值,resolve 出去
        let x = onFulfilled(this.value);
        resolve(x);
      }

      // 失败状态,直接 reject
      if (this.status === MyPromise.REJECTED) {
        // 将 onRejected 函数的返回值,reject 出去
        let x = onRejected(this.reason)
        reject && reject(x);
      }

      // 等待状态,将 onFulfilled,onRejected 塞入数组中,等待回调执行
      if (this.status === MyPromise.PENDING) {
        this.resolvedQueues.push((value) => {
          let x = onFulfilled(value);
          resolve(x);
        })
        this.rejectedQueues.push((reason) => {
          let x = onRejected(reason);
          reject && reject(x);
        })
      }
    });
    return promise2;
  }
}

// 输出结果 resolved -> then1 -> then2
复制代码

存在问题

到这里我们就完成了简单的链式调用,但是只能支持同步的链式调用,如果我们需要在 then 方法中再去进行其他异步操作的话,上面的代码就 GG 了。

如下代码:

const p1 = new MyPromise((resolved, rejected) => {
  resolved('我 resolved 了');  
});

p1.then((res) => {
  console.log(res);
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then1');
    }, 1000)
  });
})
.then((res) => {
  console.log(res);
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  console.log(res);
  return 'then3';
})
复制代码

上面的代码会直接将 Promise 对象直接当作参数传给下一个 then 函数,而我们其实是想要将这个 Promise 的处理结果传递下去。

 

第三版(异步链式调用)

这一版我们来实现 promise 的异步链式调用。

思路

先看一下 thenonFulfilledonRejected 返回的值:

// 成功的函数返回
let x = onFulfilled(this.value);

// 失败的函数返回
let x = onRejected(this.reason);
复制代码

从上面的的问题中可以看出,x 可以是一个 普通值,也可以是一个 Promise 对象,普通值的传递我们在 第二版 已经解决了,现在需要解决的是当 x 返回一个 Promise 对象的时候该怎么处理。

其实也很简单,当 x 是一个 Promise 对象的时候,我们需要进行等待,直到返回的 Promise 状态变化的时候,再去执行之后的 then 函数,代码如下:

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}

    // then 方法返回一个新的 promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 成功状态,直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 将 onFulfilled 函数的返回值,resolve 出去
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }

      // 失败状态,直接 reject
      if (this.status === MyPromise.REJECTED) {
        // 将 onRejected 函数的返回值,reject 出去
        let x = onRejected(this.reason)
        resolvePromise(promise2, x, resolve, reject);
      }

      // 等待状态,将 onFulfilled,onRejected 塞入数组中,等待回调执行
      if (this.status === MyPromise.PENDING) {
        this.resolvedQueues.push(() => {
          let x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        })
        this.rejectedQueues.push(() => {
          let x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        })
      }
    });
    return promise2;
  }
}
复制代码

我们新写一个函数 resolvePromise,这个函数是用来处理异步链式调用的核心方法,他会去判断 x 返回值是不是 Promise 对象,如果是的话,就直到 Promise 返回成功之后在再改变状态,如果是普通值的话,就直接将这个值 resovle 出去:

const resolvePromise = (promise2, x, resolve, reject) => {
  if (x instanceof MyPromise) {
    const then = x.then;
    if (x.status == MyPromise.PENDING) {
      then.call(x, y => {
        resolvePromise(promise2, y, resolve, reject);
      }, err => {
        reject(err);
      })
    } else {
      x.then(resolve, reject);
    }
  } else {
    resolve(x);
  }
}
复制代码

代码说明

resolvePromise

resolvePromise 接受四个参数:

  • promise2then 中返回的 promise
  • xthen 的两个参数 onFulfilled 或者 onRejected 的返回值,类型不确定,有可能是普通值,有可能是 thenable 对象;
  • resolverejectpromise2 的。

then 返回值类型

xPromise 的时,并且他的状态是 Pending 状态,如果 x 执行成功,那么就去递归调用 resolvePromise 这个函数,将 x 执行结果作为 resolvePromise 第二个参数传入;

如果执行失败,则直接调用 promise2reject 方法。

 

到这里我们基本上一个完整的 promise,接下来我们需要根据 Promises/A+ 来规范一下我们的 Promise

 

规范 Promise

前几版的代码笔者基本上是按照规范来的,这里主要讲几个没有符合规范的点。

规范 then(规范 2.2)

thenonFulfilledonRejected 需要异步执行,即放到异步任务中去执行(规范 2.2.4)

实现

我们需要将 then 中的函数通过 setTimeout 包裹起来,放到一个宏任务中去,这里涉及了 jsEventLoop,大家可以去看看相应的文章,如下:

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    // ...
    // then 方法返回一个新的 promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 成功状态,直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 将 onFulfilled 函数的返回值,resolve 出去
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch(err) {
            reject(err);
          }
        })
      }

      // 失败状态,直接 reject
      if (this.status === MyPromise.REJECTED) {
        // 将 onRejected 函数的返回值,reject 出去
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject);
          } catch(err) {
            reject(err);
          }
        })
      }

      // 等待状态,将 onFulfilled,onRejected 塞入数组中,等待回调执行
      if (this.status === MyPromise.PENDING) {
        this.resolvedQueues.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch(err) {
              reject(err);
            }
          })
        })
        this.rejectedQueues.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise2, x, resolve, reject);
            } catch(err) {
              reject(err);
            }
          })
        })
      }
    });
    return promise2;
  }
}
复制代码

使用微任务包裹

但这样还是有一个问题,我们知道其实 Promise.then 是属于微任务的,现在当使用 setTimeout 包裹之后,就相当于会变成一个宏任务,可以看下面这一个例子:

var p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');
})

setTimeout(() => {
  console.log('---setTimeout---');
}, 0);

p1.then(res => {
  console.log('---then---');
})

// 正常 Promise:then -> setTimeout
// 我们的 Promise:setTimeout -> then
复制代码

输出顺序不一样,原因是因为现在的 Promise 是通过 setTimeout 宏任务包裹的。

我们可以改进一下,使用微任务来包裹 onFulfilledonRejected,常用的微任务有 process.nextTickMutationObserverpostMessage 等,我们这个使用 postMessage 改写一下:

// ...
if (this.status === MyPromise.RESOLVED) {
  // 将 onFulfilled 函数的返回值,resolve 出去
  // 注册一个 message 事件
  window.addEventListener('message', event => {
    const { type, data } =  event.data;

    if (type === '__promise') {
      try {
        let x = onFulfilled(that.value);
        resolvePromise(promise2, x, resolve, reject);
      } catch(err) {
        reject(err);
      }
    }
  });
  // 立马执行
  window.postMessage({
    type: '__promise',
  }, "http://localhost:3001");
}

// ...
复制代码

实现方法很简单,我们监听windowmessage 事件,并在之后立马触发一个 postMessage 事件,这个时候其实 then 中的回调函数已经在微任务队列中了,我们重新运行一下例子,可以看到输出的顺序变为了 then -> setTimeout

当然 Promise 内部实现肯定没有这么简单,笔者在这里只是提供一种思路,大家有兴趣可以去研究一波。

规范 resolvePromise 函数(规范 2.3)

重复引用

重复引用,当 xpromise2 是一样的,那就需要报一个错误,重复应用。(规范 2.3.1)

因为自己等待自己完成是永远都不会有结果的。

const p1 = new MyPromise((resolved, rejected) => {
  resolved('我 resolved 了');  
});

const p2 = p1.then((res) => {
  return p2;
});
复制代码

x 的类型

大致分为一下这么几条:

  • 2.3.2:当 x 是一个 Promise,那么就等待 x 改变状态之后,才算完成或者失败(这个也属于 2.3.3,因为 Promise 其实也是一个 thenable 对象)
  • 2.3.3:当 x 是一个对象 或者 函数的时候,即 thenable 对象,那就那 x.then 作为 then
  • 2.3.4:当 x 不是一个对象,或者函数的时候,直接将 x 作为参数 resolve 返回。

我们主要看一下 2.3.3 就行,因为 Prmise 也属于 thenable 对象,那什么是 thenable 对象呢?

简单来说就是具有 then方法的对象/函数,所有的 Promise 对象都是 thenable 对象,但并非所有的 thenable 对象并非是 Promise 对象。如下:

let thenable = {
 then: function(resolve, reject) {
   resolve(100);
 }
}
复制代码

根据 x 的类型进行处理:

  • 如果 x 不是 thenable 对象,直接调用 Promise2resolve,将 x 作为成功的结果;

  • xthenable 对象,会调用 xthen 方法,成功后再去调用 resolvePromise 函数,并将执行结果 y 作为新的 x 传入 resolvePromise,直到这个 x 值不再是一个 thenable 对象为止;如果失败则直接调用 promise2reject

if (x != null && (typeof x === 'object' || typeof x === 'function')) {
  if (typeof then === 'function') {
    then.call(x, (y) => {
      resolvePromise(promise2, y, resolve, reject);
    }, (err) => {
      reject(err);
    })
  }
} else {
  resolve(x);
}
复制代码

只调用一次

规范(Promise/A+ 2.3.3.3.3)规定如果同时调用 resolvePromiserejectPromise,或者对同一参数进行了多次调用,则第一个调用优先,而所有其他调用均被忽略,确保只执行一次改变状态。

我们在外面定义了一个 called 占位符,为了获得 then 函数有没有执行过相应的改变状态的函数,执行过了之后,就不再去执行了,主要就是为了满足规范。

x 为 Promise 对象

如果 xPromise 对象的话,其实当执行了resolve 函数 之后,就不会再执行 reject 函数了,是直接在当前这个 Promise 对象就结束掉了。

x 为 thenable 对象

x 是普通的 thenable 函数的时候,他就有可能同时执行 resolvereject 函数,即可以同时执行 promise2resolve 函数 和 reject 函数,但是其实 promise2 在状态改变了之后,也不会再改变相应的值了。其实也没有什么问题,如下代码:

// thenable 对像
{
 then: function(resolve, reject) {
   setTimeout(() => {
     resolve('我是thenable对像的 resolve');
     reject('我是thenable对像的 reject')
    })
 }
}
复制代码

完整的 resolvePromise

完整的 resolvePromise 函数如下:

const resolvePromise = (promise2, x, resolve, reject) => {
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
}
复制代码

到这里就大功告成了,开不开心,兴不兴奋!

最后我们可以通过测试脚本跑一下我们的 MyPromise 是否符合规范。

测试

有专门的测试脚本(promises-aplus-tests)可以帮助我们测试所编写的代码是否符合 Promise/A+ 的规范。

但是貌似只能测试 js 文件,所以笔者就将 ts 文件转化为了 js 文件,进行测试

在代码里面加上:

// 执行测试用例需要用到的代码
MyPromise.deferred = function() {
  let defer = {};
  defer.promise = new MyPromise((resolve, reject) => {
      defer.resolve = resolve;
      defer.reject = reject;
  });
  return defer;
}
复制代码

需要提前安装一下测试插件:

# 安装测试脚本
npm i -g promises-aplus-tests

# 开始测试
promises-aplus-tests MyPromise.js
复制代码

结果如下:

完美通过,接下去我们就可以看看 Promise 更多方法的实现了。

 

更多方法

实现上面的 Promise 之后,其实编写其实例和静态方法,相对来说就简单了很多。

实例方法

Promise.prototype.catch

实现

其实这个方法就是 then 方法的语法糖,只需要给 then 传递 onRejected 参数就 ok 了。

private catch(onRejected) {
  return this.then(null, onRejected);
}
复制代码
例子:
const p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');
})

p1.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      rejected('错误了');
    }, 1000)
  });
})
.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  return 'then3';
}).catch(error => {
  console.log('----error', error);
})

// 1s 之后输出:----error 错误了
复制代码

Promise.prototype.finally

实现

finally() 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

private finally (fn) {
  return this.then(fn, fn);
}
复制代码
例子
const p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');
})

p1.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      rejected('错误了');
    }, 1000)
  });
})
.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  return 'then3';
}).catch(error => {
  console.log('---error', error);
  return `catch-${error}`
}).finally(res => {
  console.log('---finally---', res);
})

// 输出结果:---error 错误了" -> ""---finally--- catch-错误了
复制代码

 

静态方法

Promise.resolve

实现

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

static resolve = (val) => {
  return new MyPromise((resolve,reject) => {
    resolve(val);
  });
}
复制代码
例子
MyPromise.resolve({name: 'darrell', sex: 'boy' }).then((res) => {
  console.log(res);
}).catch((error) => {
  console.log(error);
});

// 输出结果:{name: "darrell", sex: "boy"}
复制代码

Promise.reject

实现

Promise.reject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected

static reject = (val) => {
  return new MyPromise((resolve,reject) => {
    reject(val)
  });
}
复制代码
例子
MyPromise.reject("出错了").then((res) => {
  console.log(res);
}).catch((error) => {
  console.log(error);
});

// 输出结果:出错了
复制代码

Promise.all

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例,

const p = Promise.all([p1, p2, p3]);
复制代码
  • 只有 p1p2p3 的状态都变成 fulfilledp 的状态才会变成 fulfilled
  • 只要 p1p2p3 之中有一个被 rejectedp 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给p的回调函数。
实现
static all = (promises: MyPromise[]) => {
  return new MyPromise((resolve, reject) => {
    let result: MyPromise[] = [];
    let count = 0;

    for (let i = 0; i < promises.length; i++) {
      promises[i].then(data => {
        result[i] = data;
        if (++count == promises.length) {
          resolve(result);
        }
      }, error => {
        reject(error);
      });
    }
  });
}
复制代码
例子
let Promise1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise1');
  }, 2000);
});

let Promise2 = new MyPromise((resolve, reject) => {
  resolve('Promise2');
});

let Promise3 = new MyPromise((resolve, reject) => {
  resolve('Promise3');
})

let Promise4 = new MyPromise((resolve, reject) => {
  reject('Promise4');
})

let p = MyPromise.all([Promise1, Promise2, Promise3, Promise4]);

p.then((res) => {
  // 三个都成功则成功  
  console.log('---成功了', res);
}).catch((error) => {
  // 只要有失败,则失败 
  console.log('---失败了', err);
});

// 直接输出:---失败了 Promise4
复制代码

Promise.race

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);
复制代码

只要 p1p2p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

实现
static race = (promises) => {
  return new Promise((resolve,reject)=>{
    for(let i = 0; i < promises.length; i++){
      promises[i].then(resolve,reject)
    };
  })
}
复制代码
例子

例子和 all 一样,调用如下:

// ...

let p = MyPromise.race([Promise1, Promise2, Promise3, Promise4])

p.then((res) => { 
  console.log('---成功了', res);
}).catch((error) => {
  console.log('---失败了', err);
});

// 直接输出:---成功了 Promise2
复制代码

Promise.allSettled

此方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);
复制代码

只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,而且该方法的状态只可能变成 fulfilled

此方法与 Promise.all 的区别是 all 无法确定所有请求都结束,因为在 all 中,如果有一个被 Promiserejectedp 的状态就立马变成 rejected,有可能有些异步请求还没走完。

实现
static allSettled = (promises: MyPromise[]) => {
  return new MyPromise((resolve) => {
    let result: MyPromise[] = [];
    let count = 0;
    for (let i = 0; i < promises.length; i++) {
      promises[i].finally(res => {
        result[i] = res;
        if (++count == promises.length) {
          resolve(result);
        }
      })
    }
  });
}
复制代码
例子

例子和 all 一样,调用如下:

let p = MyPromise.allSettled([Promise1, Promise2, Promise3, Promise4])

p.then((res) => {
  // 三个都成功则成功  
  console.log('---成功了', res);
}, err => {
  // 只要有失败,则失败 
  console.log('---失败了', err);
})

// 2s 后输出:---成功了 (4) ["Promise1", "Promise2", "Promise3", "Promise4"]
复制代码

 

总结

这篇文章笔者带大家一步一步的实现了符合 Promise/A+ 规范的的 Promise,看完之后相信大家基本上也能够自己独立写出一个 Promise 来了。

最后通过几个问题,大家可以看看自己掌握的如何:

  • Promise 中是如何实现回调函数返回值穿透的?
  • Promise 出错后,是怎么通过 冒泡 传递给最后那个捕获异常的函数?
  • Promise 如何支持链式调用?
  • 怎么将 Promise.then 包装成一个微任务?

实不相瞒,想要个赞!

 

参考文档

  • 阮一峰 ES6 Promise教程
  • promise 迷你书
  • Promises/A+ 规范文档
  • 剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类
  • 30分钟,让你彻底明白Promise原理
  • 手动实现一个满足promises-aplus-tests的Promise
  • 面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常

 

示例代码

示例代码可以看这里:

  • Promise 示例代码


这篇关于30分钟,带你实现一个符合规范的 Promise(巨详细)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程