2025-02-12 日报 Day95

2025-02-12 日报 Day95

Yuyang 前端小白🥬

今日的鸡汤

真正厉害的人,是在避开车马喧嚣后,还可以在心中修篱种菊;是在面对不如意时,还可以戒掉抱怨,学会自愈。

今日学习内容

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

今日笔记

1、停止和恢复执行: 使用 await 关键字之后的区别其实比看上去的还要微妙一些。比如,下面的例子中按顺序调用了 3 个函数,但它们的输出结果顺序是相反的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function foo() { 
console.log(await Promise.resolve('foo'));
}
async function bar() {
console.log(await 'bar');
}
async function baz() {
console.log('baz');
}
foo();
bar();
baz();
// baz
// bar
// foo

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别:

1
2
3
4
5
6
7
8
9
async function foo() { 
console.log(2);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3

要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。

1
2
3
4
5
6
7
8
9
10
11
12
async function foo() { 
console.log(2);
await null;
console.log(4);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3
// 4

控制台中输出结果的顺序很好地解释了运行时的工作过程:
(1) 打印 1;
(2) 调用异步函数 foo();
(3)(在 foo()中)打印 2;
(4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;
(5) foo()退出;
(6) 打印 3;
(7) 同步线程的代码执行完毕;
(8) JavaScript 运行时从消息队列中取出任务,恢复异步函数执行;
(9)(在 foo()中)恢复执行,await 取得 null 值(这里并没有使用);
(10)(在 foo()中)打印 4;
(11) foo()返回。
如果 await 后面是一个期约,则问题会稍微复杂一些。此时,为了执行异步函数,实际上会有两个任务被添加到消息队列并被异步求值。下面的例子虽然看起来很反直觉,但它演示了真正的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function foo() { 
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar() {
console.log(4);
console.log(await 6);
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
// 123458967

本例中的 Promise.resolve(8)只会生成一个异步任务。因此在新版浏览器中,这个示例的输出结果为 123458967。实际开发中,对于并行的异步操作我们通常更关注结果,而不依赖执行顺序。
2、异步函数策略:

  • 实现sleep():
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function sleep(delay) { 
    return new Promise((resolve) => setTimeout(resolve, delay));
    }
    async function foo() {
    const t0 = Date.now();
    await sleep(1500); // 暂停约 1500 毫秒
    console.log(Date.now() - t0);
    }
    foo();
    // 1502
  • 利用平行执行: 如果使用 await 时不留心,则很可能错过平行加速的机会。来看下面的例子,其中顺序等待了 5 个随机的超时:
    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
    async function randomDelay(id) { 
    // 延迟 0~1000 毫秒
    const delay = Math.random() * 1000;
    return new Promise((resolve) => setTimeout(() => {
    console.log(`${id} finished`);
    resolve();
    }, delay));
    }
    async function foo() {
    const t0 = Date.now();
    await randomDelay(0);
    await randomDelay(1);
    await randomDelay(2);
    await randomDelay(3);
    await randomDelay(4);
    console.log(`${Date.now() - t0}ms elapsed`);
    }
    foo();
    // 0 finished
    // 1 finished
    // 2 finished
    // 3 finished
    // 4 finished
    // 877ms elapsed

    async function randomDelay(id) {
    // 延迟 0~1000 毫秒
    const delay = Math.random() * 1000;
    return new Promise((resolve) => setTimeout(() => {
    console.log(`${id} finished`);
    resolve();
    }, delay));
    }
    async function foo() {
    const t0 = Date.now();
    for (let i = 0; i < 5; ++i) {
    await randomDelay(i);
    }
    console.log(`${Date.now() - t0}ms elapsed`);
    }
    foo();
    // 0 finished
    // 1 finished
    // 2 finished
    // 3 finished
    // 4 finished
    // 877ms elapsed

就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。这样可以保证执行顺序,但总执行时间会变长。
如果顺序不是必需保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。比如:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
async function randomDelay(id) { 
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
setTimeout(console.log, 0, `${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
const p0 = randomDelay(0);
const p1 = randomDelay(1);
const p2 = randomDelay(2);
const p3 = randomDelay(3);
const p4 = randomDelay(4);
await p0;
await p1;
await p2;
await p3;
await p4;
setTimeout(console.log, 0, `${Date.now() - t0}ms elapsed`);
}
foo();
// 1 finished
// 4 finished
// 3 finished
// 0 finished
// 2 finished
// 877ms elapsed


async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve();
}, delay));
}
async function foo() {
const t0 = Date.now();
const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
for (const p of promises) {
await p;
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 4 finished
// 2 finished
// 1 finished
// 0 finished
// 3 finished
// 877ms elapsed

// 注意,虽然期约没有按照顺序执行,但 await 按顺序收到了每个期约的值:

async function randomDelay(id) {
// 延迟 0~1000 毫秒
const delay = Math.random() * 1000;
return new Promise((resolve) => setTimeout(() => {
console.log(`${id} finished`);
resolve(id);
}, delay));
}
async function foo() {
const t0 = Date.now();
const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
for (const p of promises) {
console.log(`awaited ${await p}`);
}
console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 1 finished
// 2 finished
// 4 finished
// 3 finished
// 0 finished
// awaited 0
// awaited 1
// awaited 2
// awaited 3
// awaited 4
// 645ms elapsed
  • 串行执行期约:
    使用 async/await,期约连锁会变得很简单:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function addTwo(x) {return x + 2;} 
    function addThree(x) {return x + 3;}
    function addFive(x) {return x + 5;}
    async function addTen(x) {
    for (const fn of [addTwo, addThree, addFive]) {
    x = await fn(x);
    }
    return x;
    }
    addTen(9).then(console.log); // 19
    这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约,如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function addTwo(x) {return x + 2;} 
    async function addThree(x) {return x + 3;}
    async function addFive(x) {return x + 5;}
    async function addTen(x) {
    for (const fn of [addTwo, addThree, addFive]) {
    x = await fn(x);
    }
    return x;
    }
    addTen(9).then(console.log); // 19
  • 栈追踪与内存管理:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function fooPromiseExecutor(resolve, reject) { 
    setTimeout(reject, 1000, 'bar');
    }
    function foo() {
    new Promise(fooPromiseExecutor);
    }

    foo();
    // Uncaught (in promise) bar
    // setTimeout
    // setTimeout (async)
    // fooPromiseExecutor
    // foo
    3、小结: 期约的主要功能是为异步代码提供了清晰的抽象。可以用期约表示异步执行的代码块,也可以用期约表示异步计算的值。在需要串行异步代码时,期约的价值最为突出。作为可塑性极强的一种结构,期约可以被序列化、连锁使用、复合、扩展和重组。
    异步函数是将期约应用于 JavaScript 函数的结果。异步函数可以暂停执行,而不阻塞主线程。无论是编写基于期约的代码,还是组织串行或平行执行的异步代码,使用异步函数都非常得心应手。异步函数可以说是现代 JavaScript 工具箱中最重要的工具之一。
此页目录
2025-02-12 日报 Day95