字节面试官:你对Promise很熟是吧?试一下手写所有静态方法

引言

大家好啊,我是前端拿破轮。

在promise相关的笔面试题目中,经常考察的一种题目就是手写Promise

由于面试时间有限,所以让我们完整地实现Promise的可能性比较小。所以经常考察的便是手写Promise的6大静态方法。

这篇文章拿破轮就带着大家深入分析6大静态方法各自的功能并给出代码实现。

首先先来回顾一下Promise的静态方法主要包括以下几种:

  1. Promise.resolve
  2. Promise.reject
  3. Promise.all
  4. Promise.any
  5. Promise.allSettled
  6. Promise.race

Promise.resolve

MDN原文

Promise.resolve()静态方法将给定值解析Promise。如果值为promise,则直接返回该promise;如果值是一个thenable,则Promise.resolve()将使用它准备的两个回调调用then()方法,否则,返回的promise就将使用value。该函数将嵌套的类 Promise 对象(例如,一个将被兑现为另一个 Promise 对象的 Promise 对象)展平,转化为单个 Promise 对象,其兑现值为一个非 thenable 值。

js 复制代码
const promise1 = Promise.resolve(123);

promise1.then((value) => {
  console.log(value);
  // Expected output: 123
});

什么意思呢?总结一下就是3种情况。当我们使用Promise.resolve(value)时,其内部的处理取决于value的值。

  1. 如果value是一个proimse,则直接返回该promise;
  2. 如果value是一个thenable, 则会展开该thenable,直到resovle的值不再是thenable。
  3. 如果value是其他情况,则直接返回fulfilled的promise,值就是value。

所以手写代码其实非常简单

js 复制代码
Promise.myResolve = (value) => {
  // 如果value是一个promise,则直接返回value
  if (value instanceof Promise) {
    return value;
  }

  // 其他情况返回新的promise
  return new Promise((resolve) => resolve(value));
}

你可能会在很多地方看到说上面的写法不完善,因为没有处理thenable对象,这种说法是错误的,上面的代码已经能够非常好地模拟原生的Promise.resolve()的实现方式了。因为我们在新返回的Promiseexecutor中调用了resolve方法,这个会自动处理thenable对象,将其展开,所以不用我们额外处理。


Promise.reject

MDN原文

Promise.reject()静态方法返回一个已拒绝(rejected)的Promise对象,拒绝的原因就是给定的参数。

js 复制代码
function resolved(result) {
  console.log("Resolved");
}

function rejected(result) {
  console.error(result);
}

Promise.reject(new Error("fail")).then(resolved, rejected);
// Expected output: Error: fail

Promise.resolve不同,即使reason已经是一个Promise对象,Promise.rejected()方法也始终会将其封装在一个新的Promise对象中

js 复制代码
const p = Promise.resolve(1);
const rejected = Promise.reject(p);
console.log(rejected === p); // false
rejected.catch((v) => {
  console.log(v === p); // true
});

所以要实现Promise.reject就更简单了,直接返回一个新的Promise并在executor中直接调用reject(reason)即可。手写代码如下所示:

js 复制代码
Promise.myReject = (reason) => {
  return new Promise((_, reject) => {
    reject(reason);
  })
}

Promise.all

MDN原文

Promise.all()静态方法接受一个Promise可迭代对象 作为输入,并返回一个Promise。当所有输入的Promise都被兑现时,返回的Promise也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组 。如果输入的任何 Promise被拒绝,则返回的Promise将被拒绝,并带有第一个被拒绝的原因

什么意思呢?我们来看一个例子:

js 复制代码
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "foo");
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// Expected output: Array [3, 42, "foo"]

在上面的例子中,promise1fulfilled状态,值为3

promise2是一个普通数值,并非promise,所以我们由此可知,Promise.all在面对传入不是promise实例的对象时,应该会对其使用Promise.resolve包装成一个promise对象。

promise3pending状态,100ms后执行resolve('foo'),成为fulfilled的状态,值为foo

所以下面的Promise.all最初也是pending状态,由于有promise3pending100ms后,promise3变成fulfilled,所以Promise.all()也成为fulfilled,值为三个promise的值组成的数组。[3, 42, 'foo']

所以整个代码会再100ms后输出[3, 42, 'foo']

根据上述特性,我们不难写出以下手写实现Promise.all

js 复制代码
Promise.myAll = (promises) => {
  return new Promise((resolve, reject) => {
    // 结果数组
    const result = [];

    // fulfilled的promise数量
    let count = 0;

    // 保存promises的长度
    const len = promises.length;

    // 剪枝,如果是空数组,直接resolve
    if (len === 0) {
      resolve(result);
      return;
    }

    // 遍历promises数组
    for (let i = 0; i < len; i++) {
      // 用Promise.resolve包裹处理非promise值
      Promise.resolve(promises[i]).then((val) => {
        // 计数加1
        count++;

        // 将值加入结果数组对应位置
        result[i] = val;

        // 如果全部fulfilled,则新返回的promise也fulfilled
        if (count === len) {
          resolve(result);
        }
      })
        .catch((reason) => {
          // 有任何一个拒绝,则直接拒绝
          reject(reason);
        })
    }
  })
}

Promise.any

MDN原文

Promise.any()静态方法将以一个Promise可迭代对象作为输入,并返回一个Promise。当输入的任何一个Promise兑现时,这个返回的Promise将会兑现,并返回第一个兑现的值。当所有输入的Rromise都被拒绝(包括传递了空的可迭代对象)时,它会以一个包含拒绝原因的AggregateError拒绝。

如下示例:

js 复制代码
const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "quick"));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "slow"));

const promises = [promise1, promise2, promise3];

Promise.any(promises).then((value) => console.log(value));

// Expected output: "quick"

简单来说,Promise.any的返回值有三种情况:

  • 已拒绝 :如果传入的iterable为空的话,则返回值为已拒绝的Promise。
  • 异步兑现 :传入的iterable中有任何一个Promise被兑现时,返回的Promise就会被兑现,其兑现值是第一个兑现的Promise的兑现值
  • 异步拒绝 :传入的iterable中的Promise都被拒绝时。返回的Promise也拒绝。拒绝原因是一个AggregateError,其errors属性包含一个拒绝原因的数组。无论完成顺序如何,这些错误都是按照**传入的Promise的顺序排序。如果传递的iterable是非空的,但不包含待定pending的Promise,则返回的Promise**仍然是异步拒绝的(而不是同步拒绝的)。

Promise.any() 会以第一个兑现的 Promise 来兑现,即使有 Promise 先被拒绝。这与 Promise.race() 不同,后者会使用第一个敲定的 Promise 来兑现或拒绝。

js 复制代码
const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
})

const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
}) 

const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
})

Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);
  // pFast 第一个兑现
})

// 很快完成
js 复制代码
const failure1 = new Promise((resolve, reject) => {
  reject("总是失败");
});

const failure2 = Promise.reject("我也总是失败");

Promise.any([failure1, failure2]).catch((err) => {
  console.log(err);
  console.log(err.errors);
});
// [AggregateError: All promises were rejected] {
//   [errors]: [ '总是失败', '我也总是失败' ]
// }
// [ '总是失败', '我也总是失败' ]

根据上述分析,不难实现手写如下代码

js 复制代码
Promise.myAny = (promises) => {
  return new Promise((resolve, reject) => {
    const errors = [];
    let rejectedCount = 0;
    const len = promises.length;

    if (len === 0) {
      return reject(new AggregateError(errors, 'All promises were rejected'));
    }

    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i])
        .then((val) => {
          resolve(val);
        })
        .catch((err) => {
          errors[i] = err;
          rejectedCount++;
          if (rejectedCount === len) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        })
    }
  })
}

Promise.allSettled

MDN原文

Promise.allSettled()静态方法将一个Promise可迭代对象作为输入,并返回一个单独的Promise。当所有输入的Promise都已经敲定(包括传入空的可迭代对象时),返回的Promise将被兑现,并带有描述每个Promise结果的对象数组。

注意:Promise.allSettled()的返回结果永远不可能是rejected

js 复制代码
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 100, 'foo');
})

const promises = [promise1, promise2];

Promise.allSettled(promises).then((val) => {
  console.log(val);
})

// [
//   { status: 'fulfilled', value: 3 },
//   { status: 'rejected', reason: 'foo' }
// ]

这里要注意Promise.allSettled()返回的Promise状态对空数组的处理和Promise.any()是不同的。Promise.allSettled()面对传入的是空数组的情况下会返回已兑现fulfilled的promise。而Promise.any()则是返回已拒绝,这里一定要注意。

总结来看,Promise.allSettled()的返回值是一个Promise只会有两种状态:

  • 已兑现(already fulfilled) ,如果传入的iterable为空的h话
  • 异步兑现(asyncChronously fulfill) ,当给定的iterable中所有的promise已经敲定(settled)时。兑现值是一个对象数组 ,其中的对象按照iterable中传递的promise的顺序,描述每一个promise的结果,无论完成的顺序如何。每个结果对象都有以下属性:
    • status:一个字符串,要么是fulfilled,要么是rejected,表示promise的最终状态。
    • value:仅当statusfulfilled,才存在。promise的兑现值。
    • reason:仅当statusrejected,才存在。promise的拒绝原因。

根据上述描述,我们不难写出如下手写代码:

js 复制代码
Promise.myAllSettled = (promises) => {
  return new Promise((resolve) => {
    // 结果数组
    const result = [];

    // 记录promises长度
    const len = promises.length;

    // 如果为0,则进行剪枝
    if (len === 0) {
      resolve(result);
      return;
    }

    // settled状态的promise数量
    let settledCount = 0;

    // 遍历promises
    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i]).then((val) => {
        result[i] = {
          status: 'fulfilled',
          value: val
        };
      })
        .catch((err) => {
          result[i] = {
            status: 'rejected',
            reason: err
          }
        })
        .finally(() => {
          settledCount++;
          if (settledCount === len) {
            resolve(result);
          }
        })
    }
  })
}

Promise.race

MDN原文

Promise.race()静态方法接受一个promise可迭代对象,并返回一个Promise。这个返回的Promise的状态会随着第一个Promise的敲定而敲定。

看下面的例子

js 复制代码
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
})

Promise.race([promise1, promise2]).then((value) => {
  console.log(value);   // two
})

Promise.race()的返回值,会根据iterable中第一个敲定的promise的状态异步敲定。换句话说,如果第一个敲定的 promise 被兑现,那么返回的 promise 也会被兑现;如果第一个敲定的 promise 被拒绝,那么返回的 promise 也会被拒绝。

如果传入的iterable为空,返回的promise就会一直保持待定状态 。如果传入的iterable非空但其中没有任何一个 promise 是待定状态,返回的 promise 仍会异步敲定(而不是同步敲定)。

根据上述描述,容易写出以下代码

js 复制代码
Promise.myRace = (promises) => {
  return new Promise((resolve, reject) => {
    // 获取数组长度
    const len = promises.length;

    // 如果为空数组,则永远保持pending
    if (len === 0) return;

    // 遍历promises
    for (let i = 0; i < len; i++) {
      Promise.resolve(promises[i]).then(resolve, reject);
    }
  })
}

总结

本文总结了Promise的6种静态方法的特性,并实现了使用js手写模拟实现。六种静态方法中Promise.resolve()Promise.reject()是根据传入的值得到一个promise,而剩下的Promise.all()Promise.any()Promise.allSettled()Promise.race()都是用来进行并发控制的静态方法,他们的参数往往是一个promise的数组。

这里要尤其注意一下对于空数组的处理

如果传入的是空数组,四个并发控制方法返回情况如下:

  • Promise.all():已兑现(already fulfilled),同步实现
  • Promise.any():已拒绝(already rejected),同步实现
  • Promise.allSettled():已兑现(already fulfilled),同步实现
  • Promise.race():一直保持待定(pending)状态

好了,这篇文章就到这里啦,如果对您有所帮助,欢迎点赞,收藏,分享👍👍👍。您的认可是我更新的最大动力。由于笔者水平有限,难免有疏漏不足之处,欢迎各位大佬评论区指正。

往期推荐✨✨✨

我是前端拿破轮,关注我,一起学习前端知识,我们下期见!

相关推荐
LinDaiuuj39 分钟前
最新的前端技术和趋势(2025)
前端
一只小风华~1 小时前
JavaScript 函数
开发语言·前端·javascript·ecmascript·web
程序猿阿伟2 小时前
《不只是接口:GraphQL与RESTful的本质差异》
前端·restful·graphql
若梦plus3 小时前
Nuxt.js基础与进阶
前端·vue.js
樱花开了几轉3 小时前
React中为甚么强调props的不可变性
前端·javascript·react.js
风清云淡_A3 小时前
【REACT18.x】CRA+TS+ANTD5.X实现useImperativeHandle让父组件修改子组件的数据
前端·react.js
小飞大王6663 小时前
React与Rudex的合奏
前端·react.js·前端框架
若梦plus4 小时前
React之react-dom中的dom-server与dom-client
前端·react.js
若梦plus4 小时前
react-router-dom中的几种路由详解
前端·react.js
若梦plus4 小时前
Vue服务端渲染
前端·vue.js