Promise上的一些工具函数的实现

前言

之前写过手写 Promise 的实现,这次来写写它的一些工具函数和对象函数的实现

本文基于之前Promise以及相关then方法。

之前自定义的Promise函数名字为MyPromise,下文都以MyPromise来表示

正文

Promise.resolve

Promise.resolve - MDN

根据文档上的解析

Promise.resolve(value)方法返回一个以给定值解析后的Promise对象。如果这个值是一个Promise,那么将返回这个Promise;如果这个值是thenable(即带有"then" 方法),返回的Promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的Promise将以此值完成。此函数将类Promise对象的多层嵌套展平。

可以看到Promise.resolve是以参数value的类型来决定如何处理返回的Promise,而不是一味的resolve掉这个Promise

看到网上有如下的实现

1
2
3
4
5
MyPromise.resolve = function (val) {
return new MyPromise((resolve, reject) => {
resolve(val);
});
};

这样实现是百分百不对的,可以测试下面的代码

1
2
3
4
Promise.resolve(Promise.reject(1)).then(
(val) => console.log(`val: ${val}`),
(err) => console.log(`err: ${err}`)
);

输出是执行第二个参数的回调,也就是输出err: 1,如下

所以Promise.resolve只是通过传入参数value来决定,所以实现如下

1
2
3
4
5
6
7
MyPromise.resolve = function (val) {
let returnPromise;
return (returnPromise = new MyPromise((resolve, reject) => {
// MyPromiseResolve是之前实现Promise实现的
MyPromiseResolve(returnPromise, val, resolve, reject);
}));
};

Promise.reject

Promise.reject - MDN

Promise.reject就没有Promise.resolve复杂,它的逻辑就非常单纯,就是以传入的值拒绝返回的Promise对象即可,比如

1
2
3
4
Promise.reject(Promise.resolve(1)).then(
(res) => console.log(`res: ${res}`),
(err) => console.log(`err: ${err}`)
);

上面这个Promise会以Promise.resolve(1)作为失败原因被拒绝

那么实现如下

1
2
3
4
5
MyPromise.reject = function (val) {
return new MyPromise((resolve, reject) => {
reject(val);
});
};

Promise.prototype.catch

Promise.prototype.catch - MDN

catch方法用于注册一个Promisereject时的回调,所以可以使用then方法来实现catch(可以说catch是一个特殊的then

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

由于then会返回新的Promise,所以无需我们去创建一个新的Promise返回

Promise.prototype.finally

Promise.prototype.finally - MDN

finally方法注册的回调不管在resolved状态或者rejected状态都会进行执行,所以根据catch很容易写出下面的实现代码

1
2
3
MyPromise.prototype.finally = function (onFinally) {
return this.then(onFinally, onFinally);
};

但这么写其实是有问题的,在提案上(现在已经为ecma的标准了)也有解释为啥不适用then(f, f)这种方式

finally方法的提案地址:tc39 / proposal-Promise-finally

README.md中有一段

Why not .then(f, f)?

为什么不使用.then(f, f)

Promise.finally(func) is similar to Promise.then(func, func), but is different in a few critical ways:

Promise.finally(func)Promise.then(func, func)有点类似,但是在一些比较关键的地方有所不同。

  • When creating a function inline, you can pass it once, instead of being forced to either declare it twice, or create a variable for it
  • A finally callback will not receive any argument, since there’s no reliable means of determining if the Promise was fulfilled or rejected. This use case is for precisely when you do not care about the rejection reason, or the fulfillment value, and so there’s no need to provide it.
  • Unlike Promise.resolve(2).then(() => {}, () => {}) (which will be resolved with undefined), Promise.resolve(2).finally(() => {}) will be resolved with 2.
  • Similarly, unlike Promise.reject(3).then(() => {}, () => {}) (which will be resolved with undefined), Promise.reject(3).finally(() => {}) will be rejected with 3.
  • 当创建一个内联的函数,你只需要传递一次,而不需要强制传递两次声明的函数,或者不需要为这个函数创建一个变量来保存
  • 一个finally回调不会接收任何参数,因为没有可靠的方法来判断Promise是否已经被解决或者被拒绝。当你不关心Promise被拒绝的原因或者被解决的值时使用finally会更加精确。所以不需要提供参数。
  • 不像Promise.resolve(2).then(() => {}, () => {})(这个Promise将会以undefined为值被解决),Promise.resolve(2).finally(() => {})将会以2为值被解决
  • 类似地,不像Promise.reject(3).then(() => {}, () => {})(这个Promise将会以undefined为值被解决),Promise.reject(3).finally(() => {})将会以3为值被拒绝

However, please note: a throw (or returning a rejected Promise) in the finally callback will reject the new Promise with that rejection reason.

然而,请注意:在finally中抛出(或者返回一个被拒绝的Promise对象)会使得新的Promise对象以这个原因被拒绝。

我们可以测试下原生的 Promise 的情况

情况 1

1
2
3
4
5
6
7
const Promise = Promise.resolve("Promise.resolve");
// 这里finally不返回
Promise.finally(() => {
console.log("finally");
}).then((res) => {
console.log(res);
});

这时根据规范,最后一个thenonResolved中的res应该是"Promise.resolve"

测试结果符合规范

情况 2

1
2
3
4
5
6
7
8
const Promise = Promise.resolve("Promise.resolve");
// 这里finally返回一个resolved的Promise
Promise.finally(() => {
console.log("finally");
return Promise.resolve("finally Promise.resolve");
}).then((res) => {
console.log(res);
});

这时根据规范,最后一个thenonResolved中的res应该依然是"Promise.resolve",而不是"finally Promise.resolve"

测试结果符合规范

情况 3

1
2
3
4
5
6
7
8
9
10
11
12
13
const Promise = Promise.resolve("Promise.resolve");
// 这里finally返回一个rejected的Promise
Promise.finally(() => {
console.log("finally");
return Promise.reject("finally Promise.reject");
}).then(
(res) => {
console.log(`res: ${res}`);
},
(err) => {
console.log(`err: ${err}`);
}
);

这时根据规范,finally中返回了一个rejectPromise,那么要以这个Promise的拒绝原因来拒绝新的Promise对象。

那么最后一个then应该走catch的回调,然后对应参数err"finally Promise.reject"

综上,我们讨论了Promise.resolve情况下finally的情况,还有Promise.reject情况下finally的情况。

但这两种情况对于finally的情况是一样的,所以这里不做测试,大家可以自行测试下。

所以,实现上必须和上面的行为保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MyPromise.prototype.finally = function (onFinally) {
return this.then(
(res) =>
MyPromise.resolve(onFinally()).then(
// finally回调正常,穿透原来的值
() => res,
// finally回调异常,抛出该异常
(err) => {
throw err;
}
),
(err) =>
MyPromise.resolve(onFinally()).then(
// finally回调正常,穿透原来的错误
() => {
throw err;
},
// finally回调异常,抛出该异常
(_err) => {
throw _err;
}
)
);
};

可以看到实现都是通过MyPromise.resolve来包装onFinally执行后的返回值,对于resolved的情况就返回原来Promisevalue或者err,如果rejected那么抛出对应原因,也就符合了规范

可以发现,第二个参数的回调都是直接返回传入的值,所以可以省略

1
2
3
4
5
6
7
8
9
MyPromise.prototype.finally = function (onFinally) {
return this.then(
(res) => MyPromise.resolve(onFinally()).then(() => res),
(err) =>
MyPromise.resolve(onFinally()).then(() => {
throw err;
})
);
};

Promise.all

Promise.all - MDN

MDN 上对Promise.all的解释如下:

Promise.all(iterable)方法返回一个Promise实例,此实例在iterable参数内所有的Promise都“完成(resolved)”或参数中不包含Promise时回调完成(resolve);如果参数中Promise有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败Promise的结果。

简单点讲,就是全部成功,返回的Promise才成功,只要一个失败,返回的Promise就失败。

那么对于全部这个状态,就需要有一个变量来计算当前是否已经全部完成。

以及对于数组中的非Promise对象,要包装成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
MyPromise.prototype.all = function PromiseAll(promiseArr) {
// 这里做了点小判断,支持直接传入一个Promise。
// 然后浅拷贝一份,防止数组内的Promise动态的改变数组长度导致逻辑错误。
if (!Array.isArray(promiseArr)) {
promiseArr = [promiseArr];
} else {
promiseArr = promiseArr.slice();
}
return new MyPromise((resolve, reject) => {
let completeCount = 0;
const result = [];
const len = promiseArr.length;
for (let i = 0; i < len; i++) {
// 统一通过Promise.resolve包装,就可以忽略非Promise对象
MyPromise.resolve(promiseArr[i]).then(
(res) => {
// 把结果放在数组中的对应地方
result[i] = res;
// 计数判断是否完成
completeCount++;
if (completeCount === len) {
// 已经全部解决了,用result来解决返回的Promise
resolve(result);
}
},
(err) => {
reject(err);
}
);
}
});
};

Promise.allSettled

Promise.allSettled - MDN

MDN 上解释如下

Promise.allSettled()方法返回一个在所有给定的Promise都已经fulfilledrejected后的Promise,并带有一个对象数组,每个对象表示对应的Promise结果。

当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个Promise的结果时,通常使用它。

简单点讲就是每个回调不管成功失败都放进结果,然后全部处理之后在解决返回的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
MyPromise.allSettled = function (promiseArr) {
// 相同的处理
if (!Array.isArray(promiseArr)) {
promiseArr = [promiseArr];
} else {
promiseArr = promiseArr.slice();
}
return new MyPromise((resolve, reject) => {
const len = promiseArr.length;
const result = [];
let completeCount = 0;
for (let i = 0; i < promiseArr.length; i++) {
// 这里分别在成功回调,失败回调在result对应位置写入成功结果或者失败原因
// 在finally回调来计数,因为不管成功失败都要进行计数,然后一旦数组内的Promise全部执行完成,就解决返回的Promise
MyPromise.resolve(promiseArr[i])
.then(
(res) => {
result[i] = {
status: "fulfilled",
value: res,
};
},
(err) => {
result[i] = {
status: "rejected",
value: err,
};
}
)
.finally(() => {
completeCount++;
if (completeCount === len) {
resolve(result);
}
});
}
});
};

上面的实现中,每个对象为{ status: "Promise的状态", value: "对应的值"}

这是规范中所定义的,目前Promise.allSettled已经在 es2020 中添加了

规范地址:proposal-Promise-allSettled

Promise.any

Promise.any - MDN

MDN 上的解释如下

接收一个Promise可迭代对象,只要其中的一个Promise成功,就返回那个已经成功的Promise。如果可迭代对象中没有一个Promise成功(即所有的Promises都失败/拒绝),就返回一个失败的PromiseAggregateError类型的实例,它是Error的一个子类,用于把单一的错误集合在一起。本质上,这个方法和Promise.all()是相反的。

解释上也很明显指出了,“和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
MyPromise.any = function (promiseArr) {
if (!Array.isArray(promiseArr)) {
promiseArr = [promiseArr];
} else {
promiseArr = promiseArr.slice();
}
return new MyPromise((resolve, reject) => {
const len = promiseArr.length;
const errResult = [];
let completeCount = 0;
for (let i = 0; i < promiseArr.length; i++) {
// 这里和Promise.all相反,在reject回调中计数,在resolve回调中直接解决。
MyPromise.resolve(promiseArr[i]).then(
(res) => {
resolve(res);
},
(err) => {
errResult[i] = err;
completeCount++;
// 如果全部被拒绝
if (completeCount === len) {
// 拒绝返回的Promise
reject({
status: "AggregateError: No Promise in Promise.any was resolved",
errResult,
});
}
}
);
}
});
};

对于拒绝对象{ status: "AggregateError: No Promise in Promise.any was resolved", errArray}

为提案中所定义的,所以这里我们直接构造即可,该 API 会在 es2021 中被加入

proposal-Promise-any

Promise.race

Promise.race - MDN

MDN 上解释如下

Promise.race(iterable) 方法返回一个Promise,一旦迭代器中的某个Promise解决或拒绝,返回的Promise就会解决或拒绝。

简单点讲就是竞速,谁快谁决定,这个是比较好实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MyPromise.race = function PromiseRace(promiseArr) {
if (!Array.isArray(promiseArr)) {
promiseArr = [promiseArr];
} else {
promiseArr = promiseArr.slice();
}
return new MyPromise((resolve, reject) => {
for (let i = 0; i < promiseArr.length; i++) {
// 谁快谁解决,由于Promise在被resolve之后就不可变了,所以之后的resolve操作不会影响
MyPromise.resolve(promiseArr[i]).then(
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
}
});
};