
2025-02-12 日报 Day95

今日的鸡汤
真正厉害的人,是在避开车马喧嚣后,还可以在心中修篱种菊;是在面对不如意时,还可以戒掉抱怨,学会自愈。
今日学习内容
1、JS 红皮书 P353-360 第十一章:期约与异步函数
今日笔记
1、停止和恢复执行: 使用 await 关键字之后的区别其实比看上去的还要微妙一些。比如,下面的例子中按顺序调用了 3 个函数,但它们的输出结果顺序是相反的:
1 | async function foo() { |
async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别:
1 | async function foo() { |
要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。
1 | async function foo() { |
控制台中输出结果的顺序很好地解释了运行时的工作过程:
(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 | async function foo() { |
本例中的 Promise.resolve(8)只会生成一个异步任务。因此在新版浏览器中,这个示例的输出结果为 123458967。实际开发中,对于并行的异步操作我们通常更关注结果,而不依赖执行顺序。
2、异步函数策略:
- 实现sleep():
1
2
3
4
5
6
7
8
9
10async 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
47async 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 | async function randomDelay(id) { |
- 串行执行期约:
使用 async/await,期约连锁会变得很简单:这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约,如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:1
2
3
4
5
6
7
8
9
10function 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); // 191
2
3
4
5
6
7
8
9
10async 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 - 栈追踪与内存管理: 3、小结: 期约的主要功能是为异步代码提供了清晰的抽象。可以用期约表示异步执行的代码块,也可以用期约表示异步计算的值。在需要串行异步代码时,期约的价值最为突出。作为可塑性极强的一种结构,期约可以被序列化、连锁使用、复合、扩展和重组。
1
2
3
4
5
6
7
8
9
10
11
12
13function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, 'bar');
}
function foo() {
new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
// setTimeout
// setTimeout (async)
// fooPromiseExecutor
// foo
异步函数是将期约应用于 JavaScript 函数的结果。异步函数可以暂停执行,而不阻塞主线程。无论是编写基于期约的代码,还是组织串行或平行执行的异步代码,使用异步函数都非常得心应手。异步函数可以说是现代 JavaScript 工具箱中最重要的工具之一。