likes
comments
collection
share

手写Promise--7k字从0开始看完必会

作者站长头像
站长
· 阅读数 19

概述

现如今,Promise可谓大行其道。如果不太理解Promise的相关特性,你很难阅读别人的代码,也无法优雅的进行异步编程。

Promise的历史渊源

其实Promise的概念很早就诞生了,甚至比javascript年纪还大。在javascript中,最早的Promise机制出现在JQuery中,但是还没形成相关规范。后来CommonJS制定的Promise/A规范日渐流行。到了2012年,Promise/A+组织制定了同名的Promise规范:Promise/A+规范。ECMAScript规范中关于Promise的内容遵循的就是Promise/A+规范。所以,我们就按照Promise/A+规范,来实现我们自定义的Promise。如果我们能通过Promise/A+规范提供的测试用例,则说明,我们自定义的Promise也成为了Promise/A+规范的一种实现。

什么是Promise/A+规范

Promise/A+规范 所谓规范,实际上是一个指南,指导你如何实现。所以说他并不是具体的实现代码。这个规范其实不算长,所以值得大家一读,可以让我们对ECMAScript中的Promise有更深层次的理解。

Promise主要解决了什么问题

以往的异步编程,为了获取到异步任务的结果,我们通常是使用回调函数的形式。当异步任务有了结果,就调用这个函数将异步任务的结果交给回调函数进行处理。但是如果异步任务嵌套过多,就会造成回调地狱的情况。非常不利于代码的阅读和维护。Promise就是用来解决这个问题的,通过Promise的链式调用,我们可以优雅的进行异步编程。将嵌套过多的回调函数扁平化,链路化。

手写Promise

手写Promise至少需要我们对Promise展现的特性有足够多的了解,只有充分的了解,才能更好的实现它。经历这个过程,你至少能有以下收获:

  1. 为什么Promise可以进行链式调用。
  2. 为什么Promise可以异常穿透。
  3. 为什么传递给then的参数会是异步任务。
  4. Promise.allPromise.race是如何实现的。 .....

接下来,让我们来严格按照Promise/A+规范来实现我们自己的Promise

实现一个最基本的Promise

我们知道,Promise是一个类,用来创建一个Promise实例。我们需要传递一个执行器函数。

/*
 * @Description: implement Promise by myself.
 */
enum PromiseStateType {
  pending = "pending",
    fulfilled = "fulfilled",
    rejected = "rejected",
}
//@ts-ignore
class Promise {
  //Promise实例的状态。对应ECMAScript中的Promise实例的内部属性:[[PromiseState]]
  private state: PromiseStateType = PromiseStateType.pending;
  //履行Promise时的value。
  private value: any = undefined;
  //履行Promise时的reason。
  private reason: any = undefined;
  //二者共同组成了ECMAScript中的Promise实例的内部属性:[[PromiseResult]]
  constructor(
    executor: (
      resolve: (value: unknown) => void,
      reject: (reason: unknown) => void
    ) => void
  ) {
    let resolve: (value: unknown) => void = (value: any) => {
      //调用resolve后,将Promise的状态改为fulfilled,Promise的结果记为value.
      if (this.state === PromiseStateType.pending) {
        this.state = PromiseStateType.fulfilled;
        this.value = value;
      }
    };
    let reject: (reason: unknown) => void = (reason: any) => {
      //调用resolve后,将Promise的状态改为rejected,Promise的拒绝原因记为reason.
      if (this.state === PromiseStateType.pending) {
        this.state = PromiseStateType.rejected;
        this.reason = reason;
      }
    };
    if (typeof executor === "function") {
      try {
        //执行器函数是同步执行的。
        executor(resolve, reject);
      } catch (err) {
        reject(err);
      }
    }
  }
}

看懂这里,你至少需掌握了以下的知识点:(规范中均有提及)

  1. Promise实例存在三种状态:pending(初始态),fulfilledrejected。一个实例只能进行一次状态变更:要么是从pendingfulfilled,要么是从pendingrejected.
  2. value代表履行Promise(承诺或期约)的结果,当状态从pendingfulfilled时进行填充。

reason代表拒绝Promise的原因,当状态从pendingrejected时进行答复。二者其实就构成了ECMAScriptPromise实例中的内部属性[[PromiseResult]]的值。

  1. 创建实例的时候,需要传递一个函数,我们称为执行器函数。这个函数存在两个形式参数,并且这两个形式参数也是函数,形式参数对应的实际参数是Promise内部提供的,交由你去调用,这两个函数一个叫resolve,用来将Promise的状态转为fulfilled,并填充结果value。一个叫reject,用来将Promise的状态转为rejected,并答复原因reject
  2. 并且执行器函数executor的调用是在constructor中进行的。所以执行器函数是同步执行的。
  3. 当执行器函数执行过程中,主动抛出了错误,这个时候应该返回一个失败的Promise实例,并且reason应该为抛出的错误err。所以,我这里使用了一个try...catch的结构进行包裹,进行错误的捕获工作。

测试一下

let p1 = new Promise(resolve => {
  resolve(1);
});
let p2 = new Promise((resolve, reject) => {
  reject(1);
});
let p3 = new Promise((resolve, reject) => {});

手写Promise--7k字从0开始看完必会目前来讲,我们已经完成了我们第一个小目标,完成了Promise类的大致流程的搭建。

实现基本态then方法

then方法可以说是Promise链式调用的灵魂,其他方法如catchfinally都是then方法的变种,所以关于它的实现至关重要。

  then(onFulfilled?: (value?: any) => any, onRejected?: (reason?: any) => any) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) => {
            throw reason;
          };
    if (this.state === PromiseStateType.fulfilled) {
      simulateMicroTask(() => {
        onFulfilled!(this.value);
      });
    } else if (this.state === PromiseStateType.rejected) {
      simulateMicroTask(() => {
        onRejected!(this.reason);
      });
    } else {
      this.onFulfilledCallbacks.push(() => {
        onFulfilled!(this.value);
      });
      this.onRejectedCallbacks.push(() => {
        onRejected!(this.reason);
      });
    }
  }

我们知道,then方法可以接收两个参数,都是函数:

  1. 一个是fulfilled状态下的回调函数onFulfilled,它接收fulfilled状态下的结果,也就是我们前面提到的value
  2. 一个是rejected状态下的回调函数onRejected, 它接收rejected状态下的原因,也就是我们前面提到的reason

注意千万不要和resolvereject这两个函数弄混了。他们四个函数长得确实有点像。因为存在一定的关联性。但是onFulfilledonRejected是用于是状态变更下的回调。而resolvereject是用于变更状态。

有了这点知识,我们再来回顾上面的代码,首先,我们对传入的参数进行了一个预处理:

//当不是函数时,初始化为(value) => value
onFulfilled =
  typeof onFulfilled === "function" ? onFulfilled : (value) => value;
//当不是函数时,初始化为(reason) => { throw reason; }
onRejected =
  typeof onRejected === "function"
  ? onRejected
  : (reason) => {
    throw reason;
  };

可别小看这两行初始化的代码:onFulfilled的初始化是当then方法无实参传入,或者实参不是函数情况的处理。onRejected的初始化是Promise能进行异常穿透的根本原因!对于这两句话的理解可能还得结合后面的处理。但是我们可以先举两个小🌰:

//在浏览器中,这样也会返回一个fulfilled状态的Promise实例。
Promise.resolve(1).then();
//Promise的链式调用中,我们往往只需要在最后面进行异常的捕获(catch)就行了。这也就是我们常说的异常穿透机制。
//这个就是因为我们对onRejected的处理。
Promise.reject(1).then(() => {}).then(() => {}).then(() => {}).catch(() => {})

下面的一系列if/else的逻辑无非在表达三件事:

  1. 当我们调用then方法时,Promise实例已经是fulfill状态的时候,我们是不是直接调用onFulfill函数就行了。
  2. 当我们调用then方法时,Promise实例已经是reject状态的时候,我们是不是直接调用onReject函数就行了。
  3. 当我们调用pending时,Promise实例仍是pending的时候,我们是不是应该把回调函数都存起来在状态变更的时候,再进行调用呢!

上面三句话,可以说是Promise的精髓之一了。这是一定要弄懂的。但是我们还有着些许的疑问:

疑问一:simulateMicroTask是什么

在调用onFulfilledonRejected的时候,我们使用这个simulateMicroTask函数进行了包裹。这是为了符合规范中,onFulfilledonRejected属于异步任务的要求。当然规范其实并未限制异步任务是宏任务还是微任务。我们可以使用setTimeout这类宏任务来实现它,也能使用MutationObserver这类微任务来实现它。在这里,我采用的是在浏览器环境,使用MutationObserver来实现,因为**ECMAScript中的Promise的回调函数就是表现为微任务的。在node环境是用的是process.nextTick

const isBrowser = typeof window !== "undefined" ? true : false;
function simulateMicroTask(callback: () => void): void {
  //浏览器环境 使用MutationObserver模拟微任务
  if (isBrowser) {
    let counter = 1;
    const textNode = document.createTextNode(String(counter));
    const mutationInstance = new MutationObserver(callback);
    mutationInstance.observe(textNode, {
      characterData: true,
    });
    textNode.data = String(counter + 1);
  }
  //node环境 使用 process.nextTick模拟微任务
  else {
    //@ts-ignore
    process.nextTick(() => {
      callback();
    });
  }
}

疑问二:pending状态时,回调函数保存到哪里

pending状态时,我们需要保存回调函数。那我们应该保存在哪里呢?答:保存在实例对象上。所以我们需要在上面的构造函数中新增两行代码:手写Promise--7k字从0开始看完必会

疑问三,保存的回调函数数组的执行时机在何时

状态变更之时,便是我们回调函数执行之日。那我们会在哪里改变状态呢?答:resolvereject中。所以我们需要重新修改下resolvereject的代码:手写Promise--7k字从0开始看完必会至此,我们已经实现了基本态的Promise

实现进化态then方法

进化态then方法,主要目标是实现Promise的链式调用。首先,小问一个问题:为什么Promise可以进行链式调用?原因很简单,就是因为Promise.then方法本身会返回一个Promise实例,这个Promise实例的状态和结果跟回调函数onResolveonRejected的执行情况息息相关。根据我们上面所描述的,我们重新设计一下then方法:手写Promise--7k字从0开始看完必会这个地方就是定义then方法会返回一个Promise实例。实例的结果和状态取决于回调函数的执行情况。图中有四个回调函数的调用都被try...catch结构所包裹。这是因为规范规定:如果回调函数调用过程中,抛出错误,就应该返回失败的**Promise**,失败原因为抛出的错误。手写Promise--7k字从0开始看完必会当回调函数正常执行时,应该以回调函数的返回值作为resolve函数的实参,也就是作为Promise实例的结果value。举个🌰:

new Promise((resolve, reject) => {
  resolve(1);
}).then(res => {
  return 2;
});
//这个链式调用的最终结果是返回一个成功的Promise实例,并且成功的结果是2.

实现究极形态then方法(实则优化resolve方法)

首先说,这段逻辑属于Promise中很绕的一部分,所以很多东西只可意会不可言传。建议先多看规范。

现在我们可以已经可以实现Promise的链式调用了。但是还存在一个严重的问题:

new Promise((resolve, reject) => {
  resolve(1);
}).then(() => {
  return new Promise((resolve, reject) => {
  resolve(1);
});
})

当我们的回调函数onRejectonFulfilled返回一个Promise实例的时候:现在我们的Promise实现方案会将这个Promise作为then方法返回的Promise实例的value值。但是实际上,规范中其实对这种情况有所描述:手写Promise--7k字从0开始看完必会


这个地方我有必要说明一下,这个地方我没有按照规范的逻辑逻辑走(规范并没有说直接对回调函数的执行结果调用resolve方法,这是我自己的思路。)。首先说规范的思路:

  1. 拿到回调函数onRejectonFulfilled的返回值x.
  2. 拿到x的值,执行这个方法:**[[Resolve]](promise2, x)**。这个方法是一个伪方法。也就是说他没有具体的实现代码,只提供思路。
  3. 这个伪方法其实很简单,就是对回调函数的返回值进行分类讨论:

手写Promise--7k字从0开始看完必会 :::info

  1. 情况一,promisex指向同一个对象的时候,报错,promise就是then方法的返回值。x就是回调函数的返回结果。两者一致时报错。这就是一种套娃的情况。我先表示一下什么情况下会出现这种情况: :::
let x = new Promise((resolve, reject) => {
  resolve(1);
}).then(() => x);

:::info

  1. 情况二,当x是一个Promise对象的时候,应该以这个Promise对象的结果和状态作为最终then方法返回的Promise实例的结果和状态。 ::: :::info

  2. 情况三,就比较饶了。当return的结果是一个thenable结构的时候。应该将resolvePromise方法和rejectPromise方法传递给它:

    1. 如果rejectPromise方法被调用,则then方法返回一个失败的Promise方法。
    2. 如果resolvePromise方法被调用,则对拿到的值又走一遍[[Resolve]](promise2, x)的逻辑。实际上是形成了一种递归调用。(这地方很绕很绕,一开始是懵的,是很正常的,因为我的表达能力有限。)
    3. 如果调用过程,报错,则then方法返回失败的Promise方法。 ::: thenable的含义这里出现了一个概念:thenable。所谓thenable的含义就是对象或者函数如果实现了then方法这个接口,就属于thenable。可见,Promise对象一定是thenable。 :::info
  3. 情况四,如果x不属于上面的情况,则then方法以x为结果,返回成功的Promise实例。 :::


:::info 上面的逻辑,可以说很绕很绕。但是我觉得上面只在做一件事儿:当返回值是thenable的时候(Promise也是thenable),对其做解包。(请理解我这句话。)我们要不断地取出thenable结构中的then方法的第一个参数(这个参数是个函数),接收到的值,直到接收到的值不是一个thenable结构,则说明这个值是then方法返回的Promise实例的状态才会真正确定,并且Promise的结果正好该值。

举个🌰:

new Promise((resolve, reject) => {
  resolve(1);
}).then(() => {
  return {
    then: (a) => {
      a({
        then: (a) => {
          a(1);
        } 
      }) 
    }
  }
})

这个then方法返回的Promise实例的最终结果value应该是1,看懂这个应该会对你理解上面描述的这个[[Resolve]](promise2, x)方法会有所帮助。


总而言之,[[Resolve]](promise2, x)在进行一个解包的操作。我现在按照它的思路来实现一下这个伪方法(我见网上也有人是这样写的):

then(onFulfilled?: (value?: any) => any, onRejected?: (reason?: any) => any) {
  ......
  let promise2 = new Promise((resolve, reject) => {
    if (this.state === PromiseStateType.fulfilled) {
      simulateMicroTask(() => {
        try {
          let x = (onFulfilled!(this.value));
          promiseResolutionProcedure(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
    } 
  });
  ......
}
//这个函数就是对那个[[Resolve]](promise2, x)伪方法的具体实现。网上别人的思路。
function promiseResolutionProcedure (promise2, x, resolve, reject) {
  if (promise2 === x) { 
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  let called;
  if ((typeof x === 'object' && x != null) || 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); 
        }, r => {
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e)
    }
  } else {
    resolve(x)
  }
}

但是,我们来思考一个问题,解包(自创的名词,规范中没有这个概念。)这个动作在这里做真的好吗。因为使用Promise的同学应该都知道,resolve函数也表现了解包这种行为:

new Promise(resolve => {
  resolve(new Promise(resolve => resolve(1)))
})

手写Promise--7k字从0开始看完必会那我们为什么不把解包这个动作放到resolve函数中呢?所以我这样去实现它:

//废弃
// try {
//   let x = (onFulfilled!(this.value));
//   promiseResolutionProcedure(promise2, x, resolve, reject);
// } catch (err) {
//   reject(err);
// }
//新方案
try {
  resolve(onFulfilled!(this.value));
} catch (err) {
  reject(err);
}

那现在我们需要在resolve中实现解包,所以我们不得不重新改造一下resolve函数的实现:手写Promise--7k字从0开始看完必会 :::info

  1. 这里,我十分巧妙的使用了**this**来处理了情况一,因为**constructor**中的**this**就是指向的构造出来的**Promise**实例。
  2. 第二点,同样也巧妙,我这里并没有对**Promise**和其他**thenable**结构进行区分,因为我认为所有**thenable**的解包逻辑是一样的,根本不存在任何区别。我的处理逻辑很简单,也是通过递归。当我发现你是一个**thenable**结构的时候,我就会一直派**resolve**函数去拿到你真正的结果。只有你不是**thenable**结构的时候,**resolve**才能发挥其真正的作用,那就是改变**Promise**实例的状态,填充结果**value**。请务必注意我**return**语句的使用。
  3. 为什么then方法要进行try...catch?如果valuePromise的时候,是不可能会有同步的错误被抛出从而被try...catch...捕获到的。但是我们得考虑自定义的then方法的情况:

:::

new Promise(resolve => {
  resolve({
    then() {
      throw 1;
    }
  })
})

:::info

  1. 为什么要使用call调用then方法,而不是直接使用value.then。这二者调用时,this指向是一样的,为什么要脱裤子放屁,多此一举呢。

事实上,每次属性的访问都是有可能造成副作用的。比如当他是一个访问器属性的时候,这个时候,必然会执行一次get。如果使用value.then就会又造成一次get方法的调用。 :::

let obj = {};
let number = 0;
Object.defineProperty(obj, 'then', {
  get() {
    //这就是副作用!!!
    number++;
    return function() {
      throw 1;
    }
  }
})
new Promise((resolve, reject) => {
  then() {
      throw 1;
    }
})

至此,我们已经基本实现了resolve方法的解包功能。

完善resolve/reject方法

我们搭建的Promise方法其实已经完成了80%的工作,但是它还存在一个严重的缺陷:那就是规范中提到的这句话:手写Promise--7k字从0开始看完必会说人话就是,如果我们的resolve方法被调用过之后,之后所有的rejectresolve方法的调用将会被忽略。可能有同学会想,我们好像已经实现了这个效果:

new Promise((resolve, reject) => {
  resolve(1);
  resolve(2);
  resolve(3);
  reject(4);
})

这个结果最终会是fulfilled状态的Promise实例,并且是结果value1。 :::info 但是,我们思考一个问题,它是怎么实现忽略后续执行的resolve/reject方法的。其实后续的resolve/reject方法还是按部就班的被执行了,只是第一次执行的resolve方法改变了Promise实例的状态,导致后续的执行并没有产生实际的效果。总而言之,它是通过状态的改变,来变相规避了重复调用的问题。

但是,考虑一种情景,如果状态的改变不是同步执行的呢?这不就会出问题了吗?举个🌰:

new Promise((resolve, reject) => {
  resolve( new Promise((res) => {
    res(1);
  }));
  reject(2);
})
  1. 当我们resolve一个Promise的时候,我们会调用它的then方法,然后将resolvereject传递给then方法。但是then调用回调函数是异步的。所以状态的改变不会立即到来。
  2. 这时同步代码继续执行,reject(2),这时候Promise的状态被这行代码修改掉了。即使前面的异步任务执行也无力回天了,因为状态已经被改变了。

最后的表现效果为reject方法发挥了作用。而前面的resolve方法被覆盖了。这显然不是规范想要达到的效果。那我们应该重新思考一下了:

明确目标:我们要达到的目标是resolve/reject方法只要有一个被调用,其他调用resolve/reject都会被忽略。

我这里想到的思路是通过闭包来达到我们的目的:

function onceCall(
  resolve: (value: unknown) => void,
  reject: (reason: unknown) => void
): {
  resolve: (value: unknown) => void;
  reject: (reason: unknown) => void;
} {
  let called = false;
  return {
    resolve(vlaue) {
      if (!called) {
        called = true;
        resolve(vlaue);
      }
    },
    reject(reason) {
      if (!called) {
        called = true;
        reject(reason);
      }
    },
  };
}

这里就是使用called作为标志,只要暴露出来的resolvereject方法任意一个被调用。就会导致标志置为true,从而免疫一切后续调用。所以我们现在使用的resolvereject要被onceCall进行一次包裹,所以我们对原来的resolvereject进行一次重命名:手写Promise--7k字从0开始看完必会手写Promise--7k字从0开始看完必会resolve重命名为realResolve,将reject重命名为realReject然后对传入executor函数中的resolvereject先进行一次onceCall包裹手写Promise--7k字从0开始看完必会此时,我们代码中用到的resolvereject都是被包裹后的。只有realResolverealReject才是原始的。图中还对realResolve方法进行了一些调整:

  1. reject改为realReject

手写Promise--7k字从0开始看完必会这是因为realResolve被调用,则说明resolve方法一定被调用了。那么called已经为true了。所以这时候调用reject肯定是不起作用的,所以得调用realReject

  1. 又增加了一个try...catch...的结构,这个结构是为了捕获访问value.then时,可能会抛出的异常,举个🌰:
let value = {};
Object.defineProperty(value, 'then', {
  get() {
    throw 1;
  }
})
  1. 对传递给then方法的resolvereject也进行了一次onceCall的包裹。

其实对于Promise实例来说,传递的resolvereject不可能被重复调用。那onceCall包裹的意义在哪里呢?这是因为要考虑thenable结构的情况:

var thenableInstance = {
  then(resolve, reject) {
    resolve();
    resolve();
    reject();
  }
}

既然会出现多次调用的情况,那么也同样面临之前描述的那种问题,所以仍然需要进行一次**onceCall**的包裹。

验证Promise的规范性

我们前文说过,我们的自定义的Promise会遵循Promise/A+组织推出的Promise/A+规范。为了验证大家的Promise是否合乎规范。Promise/A+组织也有相应的NPM包来进行校验:promises-aplus-tests。该npm包中有872条测试用例,完全通过则证明合乎规范。 :::info

  1. 下载npm包,npm install promises-aplus-tests
  2. 改造我们的手写实现文件,文件尾部添加如下代码:
//@ts-ignore
Promise.defer = Promise.deferred = function () {
  let dfd = {};
  //@ts-ignore
  dfd.promise = new Promise((resolve, reject) => {
    //@ts-ignore
    dfd.resolve = resolve;
    //@ts-ignore
    dfd.reject = reject;
  });
  return dfd;
};
//@ts-ignore
module.exports = Promise;
  1. 执行命令:npx promises-aplus-tests <你的手写文件路径>

即可完成测试。

通过这个测试包,我们可以有效的排查我们未考虑到的情况,在整个代码的形成过程中,可见我其实考虑很多边界情况。这也是借助这个库帮助我完成的,不断优化代码逻辑。没有什么事情是一蹴而就的。

我已自测:手写Promise--7k字从0开始看完必会


如果想参考我的代码,请点这里!代码提交的历史是和文章思路是保持一致的。不过我使用了typescript,所以我经过了一次编译,关注promise/index.js文件即可。

完善后续API

后续API基本都是基于then方法的变种。

Promise.prototype.catch

catch(onRejected?: (reason?: any) => any) {
    return this.then(undefined, onRejected);
  }

Promise.protype.finally

 finally(callback: () => void) {
    this.then(
      (value) => {
        return new Promise((resolve) => {
          resolve(callback());
        }).then(() => value);
      },
      (reason) => {
        return new Promise((resolve) => {
          resolve(callback());
        }).then(() => {
          throw reason;
        });
      }
    );
  }

:::info 相对来讲,finally的逻辑会有点绕:

  1. 当成功的Promise调用finally方法时:
    1. 如果finally方法的回调函数不抛出错误或返回错误的Promise实例,则返回和当前成功Promise状态和结果一致的Promise
    2. 如果finally方法的回调函数抛出错误或返回错误的Promise实例,则返回状态为失败,结果为回调函数抛出的错误内容的Promise
  2. 当失败的Promise调用finally方法时:
    1. 如果finally方法的回调函数不抛出错误或返回错误的Promise实例,则返回和当前成功Promise状态和结果一致的Promise
    2. 如果finally方法的回调函数抛出错误或返回错误的Promise实例,则返回状态为失败,结果为回调函数抛出的错误内容的Promise。 :::

Promise.resolve

static resolve(value: unknown) {
    return new Promise((resolve, reject) => {
      resolve(value);
    });
  }

Promise.reject

  static reject(reason: unknown) {
    return new Promise((resolve, reject) => {
      reject(reason);
    });
  }

Promise.all

  static all(list: any[]) {
    return new Promise((resolve, reject) => {
      //暂存成功数组元素的结果,务必和数组元素是一一对应关系
      const results: any[] = [];
      //记录成功状态的数量。作为标志。
      let fulfilledCount: number = 0;
      function fulfillPromise(value: unknown, index: number) {
        results[index] = value;
        if (++fulfilledCount === list.length) {
          resolve(results);
        }
      }
      for (let index = 0; index < list.length; index++) {
        Promise.resolve(list[index]).then((value) => {
          fulfillPromise(value, index);
        }, reject);
      }
    });
  }

主要思路就是每次数组元素成功的时候,进行判断是不是所有元素都成功了,是的话就返回成功的Promise

Promise.race

  static race(list: any[]) {
    return new Promise((resolve, reject) => {
      for (let index = 0; index < list.length; index++) {
        Promise.resolve(list[index]).then(resolve, reject);
      }
    });
  }

就是比谁跑的快~

写在最后

行文至此,身心俱疲。如有纰漏,不吝赐教。(后面的API硬敲的,没测hh~卒...)