2025-02-10 日报 Day93

2025-02-10 日报 Day93

Yuyang 前端小白🥬

今日的鸡汤

如果自己的才华还撑不起梦想时,就应该静下来学习;如果自己的能力还驾驭不了目标时,就应该沉下心来历练。

今日学习内容

1、JS 红皮书 P339-344 第十一章:期约与异步函数

今日笔记

1、期约连锁与期约合成: 多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约。

  • 期约连锁: 把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方法(then()、catch()和 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
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
53
54
55
56
57
let p = new Promise((resolve, reject) => {
console.log("first");
resolve();
});
p.then(() => console.log("second"))
.then(() => console.log("third"))
.then(() => console.log("fourth"));
// first
// second
// third
// fourth

let p1 = new Promise((resolve, reject) => {
console.log("p1 executor");
setTimeout(resolve, 1000);
});
p1.then(
() =>
new Promise((resolve, reject) => {
console.log("p2 executor");
setTimeout(resolve, 1000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p3 executor");
setTimeout(resolve, 1000);
})
)
.then(
() =>
new Promise((resolve, reject) => {
console.log("p4 executor");
setTimeout(resolve, 1000);
})
);
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

// 把生成期约的代码提取到一个工厂函数中,就可以写成这样:
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve("p1 executor")
.then(() => delayedResolve("p2 executor"))
.then(() => delayedResolve("p3 executor"))
.then(() => delayedResolve("p4 executor"));
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简洁地将异步任务串行化,解决之前依赖回调的难题。假如这种情况下不使用期约,那么前面的代码可能就要这样写了:

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
function delayedExecute(str, callback = null) {
setTimeout(() => {
console.log(str);
callback && callback();
}, 1000);
}
delayedExecute("p1 callback", () => {
delayedExecute("p2 callback", () => {
delayedExecute("p3 callback", () => {
delayedExecute("p4 callback");
});
});
});
// p1 callback(1 秒后)
// p2 callback(2 秒后)
// p3 callback(3 秒后)
// p4 callback(4 秒后)

// 心明眼亮的开发者会发现,这不正是期约所要解决的回调地狱问题吗?
// 因为 then()、catch()和 finally()都返回期约,所以串联这些方法也很直观。下面的例子同时使用这 3 个实例方法:

let p = new Promise((resolve, reject) => {
console.log("initial promise rejects");
reject();
});
p.catch(() => console.log("reject handler"))
.then(() => console.log("resolve handler"))
.finally(() => console.log("finally handler"));
// initial promise rejects
// reject handler
// resolve handler
// finally handler
  • 期约图: 因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。
    下面的例子展示了一种期约有向图,也就是二叉树:
    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()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约:
    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
    53
    let 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.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
    let 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>: 5
    如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。与 Promise.all()类似,合成的期约会静默处理所有包含期约的拒绝操作,如下所示:
    1
    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
    // 没有未处理的错误
  • 串行期约合成: 期约的另一个主要特性:异步产生值并将其传给处理程序。基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这很像函数合成,即将多个函数合成为一个函数,比如:
    1
    2
    3
    4
    5
    6
    7
    function 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)); // 17
    在这个例子中,有 3 个函数基于一个值合成为一个函数。类似地,期约也可以像这样合成起来,渐进地消费一个值,并返回一个结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function 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
此页目录
2025-02-10 日报 Day93