引言

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

在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 值。

1
2
3
4
5
6
7
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。

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

1
2
3
4
5
6
7
8
9
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对象,拒绝的原因就是给定的参数。

1
2
3
4
5
6
7
8
9
10
11
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对象中

1
2
3
4
5
6
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)即可。手写代码如下所示:

1
2
3
4
5
Promise.myReject = (reason) => {
return new Promise((_, reject) => {
reject(reason);
})
}

Promise.all

MDN原文

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

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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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拒绝。

如下示例:

1
2
3
4
5
6
7
8
9
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 来兑现或拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 第一个兑现
})

// 很快完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]: [ '总是失败', '我也总是失败' ]
// }
// [ '总是失败', '我也总是失败' ]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的拒绝原因。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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的敲定而敲定。

看下面的例子

1
2
3
4
5
6
7
8
9
10
11
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 仍会异步敲定(而不是同步敲定)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)状态

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

往期推荐✨✨✨

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