
2025-02-10 日报 Day93

今日的鸡汤
如果自己的才华还撑不起梦想时,就应该静下来学习;如果自己的能力还驾驭不了目标时,就应该沉下心来历练。
今日学习内容
1、JS 红皮书 P339-344 第十一章:期约与异步函数
今日笔记
1、期约连锁与期约合成: 多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约。
- 期约连锁: 把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方法(then()、catch()和 finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。比如:
1 | let p = new Promise((resolve, reject) => { |
每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简洁地将异步任务串行化,解决之前依赖回调的难题。假如这种情况下不使用期约,那么前面的代码可能就要这样写了:
1 | function delayedExecute(str, callback = null) { |
- 期约图: 因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。
下面的例子展示了一种期约有向图,也就是二叉树:注意,日志的输出语句是对二叉树的层序遍历。如前所述,期约的处理程序是按照它们添加的顺序执行的。由于期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// A
// / \
// B C
// /\ /\
// D E F G
let A = new Promise((resolve, reject) => {
console.log('A');
resolve();
});
let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));
B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));
// A
// B
// C
// D
// E
// F
// G
树只是期约图的一种形式。考虑到根节点不一定唯一,且多个期约也可以组合成一个期约(通过下一节介绍的 Promise.all()和 Promise.race()),所以有向非循环图是体现期约连锁可能性的最准确表达。 - Promise.all()和Promise.race(): Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和 Promise.race()。而合成后期约的行为取决于内部期约的行为。
Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约:Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接受一个可迭代对象,返回一个新期约: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
41
42
43
44
45
46
47
48
49
50
51
52
53let p1 = Promise.all([
Promise.resolve(),
Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.all([3, 4]);
// 空的可迭代对象等价于 Promise.resolve()
let p3 = Promise.all([]);
// 无效的语法
let p4 = Promise.all();
// TypeError: cannot read Symbol.iterator of undefined
// 合成的期约只会在每个包含的期约都解决之后才解决:
let p = Promise.all([
Promise.resolve(),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p); // Promise <pending>
p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
// all() resolved!(大约 1 秒后)
// 如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝:
// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>
// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve(),
Promise.reject(),
Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined
// 如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]
// 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期约不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。合成的期约会静默处理所有包含期约的拒绝操作,如下所示:
// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.all([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。与 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
33let p1 = Promise.race([
Promise.resolve(),
Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.race([3, 4]);
// 空的可迭代对象等价于 new Promise(() => {})
let p3 = Promise.race([]);
// 无效的语法
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined
// Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约:
// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3
// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
Promise.reject(4),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4
// 迭代顺序决定了落定顺序
let p3 = Promise.race([
Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 51
2
3
4
5
6
7
8
9// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.race([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误 - 串行期约合成: 期约的另一个主要特性:异步产生值并将其传给处理程序。基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这很像函数合成,即将多个函数合成为一个函数,比如:在这个例子中,有 3 个函数基于一个值合成为一个函数。类似地,期约也可以像这样合成起来,渐进地消费一个值,并返回一个结果:
1
2
3
4
5
6
7function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
return addFive(addTwo(addThree(x)));
}
console.log(addTen(7)); // 171
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
return Promise.resolve(x)
.then(addTwo)
.then(addThree)
.then(addFive);
}
addTen(8).then(console.log); // 18
// 使用 Array.prototype.reduce()可以写成更简洁的形式:
function addTen(x) {
return [addTwo, addThree, addFive]
.reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(8).then(console.log); // 18
// 这种模式可以提炼出一个通用函数,可以把任意多个函数作为处理程序合成一个连续传值的期约连锁。这个通用的合成函数可以这样实现:
function compose(...fns) {
return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
let addTen = compose(addTwo, addThree, addFive);
addTen(8).then(console.log); // 18