跳到主要内容

异步编程 — 等待变成显式的

这一章讲三件事: JS 为什么不选线程而选「函数+回调」; promise 怎么把「 future 的值」变成能传递、能组合、能统一处理失败的对象; 事件循环的几条铁律,以及它们如何解释你见过的所有异步怪象。 读完你能预测「回调和 console.log 谁先跑」这类问题,而不是靠背。

1. 先看现象:等待在浪费谁

处理器算得飞快,但程序常要跟外部打交道——网络、硬盘,这些比内存慢得多。同步模型里,发起请求的函数要等到事毕才返回;要并发(多件事同时推进)就得开线程:另一个执行流,由操作系统穿插调度1

JS 两大平台(浏览器与 Node)集体选了另一条路:把耗时操作做成异步,不依赖线程。书里给的理由很直白——用线程编程是出了名的难(程序同时做几件事时,理解它做什么的难度陡增),所以「不用线程」被普遍认为是好事2

两种模型最准的对照,一句:同步模型里「等待完成」是隐式的、语言替你安排;异步模型里等待是显式的、归你控制。代价是双向的:不合直线的程序好写了,合直线的程序反而写别扭3

2. 回调:把「之后做什么」当参数递进去

回调(callback)风格:异步函数不返回结果,而是接收一个函数,事成之后调用它setTimeout(() => console.log("Tick"), 500) 是最简样板4

连做两件异步的事,就得嵌套:

function compareFiles(fileA, fileB, callback) {
readTextFile(fileA, contentA => {
readTextFile(fileB, contentB => {
callback(contentA == contentB);
});
});
}

书里点了两条痛:缩进随异步动作一路加深;更要命的是——「异步是会传染的」:任何调用了异步函数的函数,自己也必须异步,用回调或类似机制交付结果。调用回调比 return 一个值繁琐且容易错,大程序被迫这样组织就不妙了5

3. promise:一张「代表未来值」的收据

promise 是一个对象,代表一个「现在可能还没有」的值。它有 then 方法:注册「值到手时怎么办」;值到位(术语叫 resolve)时被调用——已经 resolve 的 promise,之后再 then 也照样拿到值,这个保证让时序不再敏感6

then 本身返回一个新的 promise,其值是回调的返回值(若回调返回 promise,则等它落定)——于是可以链起来:

textFile(listFile)
.then(content => content.trim().split("\n")) // 同步变换,也占一环
.then(ls => ls[Math.floor(Math.random() * ls.length)])
.then(filename => textFile(filename)); // 真正的异步一环

书里的心智模型值得单独一行:promise 让代码不用再操心「值什么时候到」——由 then 拼起来的计算,会在输入就绪时异步地流动7

失败也走同一条管道

回调时代报错的约定是 (error, value) 第一参——每个调用点都得记得检查。promise 把成败收进对象自身:resolve(成功)或 reject(失败);链上任何一环失败,整条链被标记为失败,失败点之后的所有成功处理器全部跳过;catch 注册失败处理器,而且它也返回新 promise——接住后返回的值,继续流给下游的成功处理器8

new Promise((_, reject) => reject(new Error("Fail")))
.then(v => console.log("处理器 1:", v)) // 被跳过(上游是失败态)
.catch(reason => { console.log("接住 " + reason); return "nothing"; })
.then(v => console.log("处理器 2:", v)); // → 处理器 2: nothing

规则一句话:每个链环挂着成功/失败处理器,类型对不上的自动被无视;抛出的异常、返回的失败 promise,都汇进同一条失败通道9

4. 主走查:Carla 逐位破译密码

本章的故事主角是柏林一只会编程的乌鸦 Carla。她要连上旧机场的 Wi-Fi,而认证机制有个漏洞:输入部分密码时,「前几位正确」与「首位就错」的响应不一样——于是六位密码可以逐位试出:第一位从 0 试到 9,哪个数字让接入点「不立刻拒绝」就是它;定住第一位,再试第二位……10

Carla 手上有两样工具。其一是 withTimeout(promise, time)——给请求限时,超时算失败:

function withTimeout(promise, time) {
return new Promise((resolve, reject) => {
promise.then(resolve, reject);
setTimeout(() => reject("Timed out"), time);
});
}

它能成立,靠的是 promise 只能被定一次:谁先到谁说了算,后到的 resolve/reject 一律被忽略11

其二是逐位搜索的主循环。难点在「你没法在 for 循环里等一个 promise」,所以先用递归写12:

function crackPasscode(networkID) {
function nextDigit(code, digit) {
let newCode = code + digit;
return withTimeout(joinWifi(networkID, newCode), 50)
.then(() => newCode) // 成功:整串密码到手
.catch(failure => {
if (failure == "Timed out") return nextDigit(newCode, 0); // 前缀对了!加深一位
else if (digit < 9) return nextDigit(code, digit + 1); // 这位不对,换下一个
else throw failure; // 0-9 全灭,认输
});
}
return nextDigit("", 0);
}
crackPasscode("HANGAR 2"); // → "555555"

带具体值走一遍 code="5555"digit=0..8 的八次失败(接入点约 20 毫秒就拒绝,50 毫秒超时是安全垫):每次进 catch 的「digit<9」分支;digit=9joinWifi 等了 50 毫秒没拒绝 → 超时 → 进第一分支,code 变成 "55555"、从 0 再试下一位。递归的每一层负责一位,错误分支负责换数,成功分支负责收工

这段逻辑其实是线性(一路直行)的——永远只等上一步,用异步函数重写后,双层循环的结构就显形了13:

async function crackPasscode(networkID) {
for (let code = "";;) {
for (let digit = 0;; digit++) {
let newCode = code + digit;
try {
await withTimeout(joinWifi(networkID, newCode), 50);
return newCode;
} catch (failure) {
if (failure == "Timed out") { code = newCode; break; }
else if (digit == 9) throw failure;
}
}
}
}

async 函数的机制:隐式返回 promise;体内 await 某个 promise,停在那儿,值到手再继续;promise 若 reject,异常就从 await 的位置抛出,正好落进 try/catch。这样的函数不再一口气跑完——它可以在任何 await 处冻结,稍后被唤醒14

5. 冻结与唤醒的真身:生成器

「函数可暂停可恢复」不是 async 的专利。生成器(generator)function* 定义、用 yield 让位:

function* powers(n) {
for (let current = n;; current *= n) yield current;
}
for (let power of powers(3)) { if (power > 50) break; console.log(power); }
// → 3, 9, 27

调用生成器得到一个迭代器(第 06 章的接口!);每调一次 next,函数跑到下一个 yield冻结并交出值——局部状态自动保存,所以手写迭代器类(ListIterator 那套)可以全部退休15。两条规则:yield 只能写在生成器自己体内;而 async 函数就是一种特殊的生成器——它 yield 的是 promise,await 的值就是 promise 的结果16

顺带一提 Carla 的后续:她扫描局域网找到九块 LED 屏,用 Promise.all(把一组 promise 合成一个,全完成才完成、一失败即失败)同步刷新九块屏,给整面墙放了一星期的发光乌鸦动画——一个 async play() 循环里先 await displayFrame(...)await wait(帧时长)17

6. 事件循环:同一条铁轨上跑火车

所有异步代码背后是同一套调度:主脚本与每个回调都一口气跑完,互不打断;环境像一个环绕程序的大循环(事件循环),事件发生就进队列,队列里的代码一段段执行——任何时刻只有一段在跑18

由此四条推论,每条都解释一类怪象:

  1. 回调跑在空栈上。异步行为发生在自己独立的调用栈里——这正是跨异步代码管异常如此困难的原因:回调抛异常时,你的 try/catch 根本不在栈上19;
  2. 慢代码拖住一切setTimeout(fn, 20) 之后主脚本死循环 50 毫秒,回调实际 55 毫秒才跑——计时器只保证「不早于」,不保证「准时」20;
  3. 已 resolve 的 promise 也要排队Promise.resolve("Done").then(console.log); console.log("Me first!"); 打印顺序是 Me first! 在前——then 的回调被安排成新事件,当前脚本优先跑完21;
  4. 缺口(gap)里别人会动手。见下一节。

7. 异步缺口:fileSizes 的亡魂

await 前后,程序的执行有缝隙,别的代码(别的回调)可以在缝里跑。书里给了一个完全静默的凶案:

async function fileSizes(files) {
let list = "";
await Promise.all(files.map(async fileName => {
list += fileName + ": " + (await textFile(fileName)).length + "\n";
}));
return list; // → 永远只剩一行!
}

拆解:+=语句开始时的 list 值,await 完成后再把「旧值 + 本行」写回去。map 先同步跑完所有迭代,此时 list 还是空串;各文件的取回有快有慢,但每个 += 的起点都是同一个空串——最后写回的那个(耗时最长的文件)覆盖一切22

处方照例是「算新值,别改旧值」:让每个映射返回一行,(await Promise.all(lines)).join("\n")23。书里的收尾判断对本书很关键:显式异步的一大优点,正是缺口看得见——每个 await 都是一个明确的「此处会被插队」24

8. 作者的判断与证据

  • 「线程难,JS 不用线程」:行业共识的转述,有明确归因(同时做几件事更难理解)2;
  • 「promise 让时序不敏感」:机制保证(已 resolve 再 then 仍触发),可复验6;
  • 「写异步可靠的程序难,很多人不做」:与第 08 章的异常稳健性同调,风险自负;
  • Carla 故事里的接入点「20 毫秒响应」是虚构设定的参数,书里当场写明 50 毫秒超时是照它定的。

判断(我们的,不是书里的): 回调→promise→async/await 的演进,本质是同一个东西的 三次换皮:「把剩余的计算打包起来,事成后接着跑」。promise 把打包标准化了, async/await 把打包藏起来了。读旧代码时,看到回调地狱,心里可以自动翻译成 then 链、再翻译成 await——三种写法能一一对应,理解一种等于理解三种。 如果错,会错在: 生成器手写的自定义控制流(如取消、节流)在三种写法之间不完全可译, 「一一对应」在那些场景会失真。

9. 边界与局限

  • 书里用自造的 readTextFile/request 演示——真实 API(浏览器 fetch、Node fs)在后续章节才给出;
  • Promise.raceanyfinally 等组合子未覆盖(只讲了 all);
  • 微任务与宏任务的优先级细节、「事件循环饿死」的深水区不涉及;
  • async 迭代器(for-await)不在本书范围。

10. 可带走的

  1. JS 的等待是显式的;不用线程是因为多线程难写难懂;
  2. 回调会传染;缩进地狱是它的症状;
  3. promise=代表未来值的收据;已 resolve 再 then 依然拿到值;
  4. then 返回新 promise;链上一环失败,全链短路进失败通道;catch 接住后世界继续;
  5. promise 只能定一次——withTimeout 这类「限时壳」全靠这条;
  6. for 循环里等不了 promise;递归或 async 循环是两条出路;
  7. async/await=生成器的语法糖;await 处冻结、结果到达处唤醒;
  8. 事件循环:单轨火车;回调跑在空栈上;慢代码拖一切;"Me first!" 是试金石;
  9. await 两侧是缺口:+= 跨缺口会互相覆盖;算新值别改旧值;
  10. Promise.all=并发刷一批、全到齐才继续。

11. 原文地图

主题原书章原文位置
同步/异步/线程取舍Asynchronous Programmingtext/14-fm-asynchronous-programming.txt:18(搜「synchronous」) · :26(搜「thread」) · :34(搜「notoriously」)
显式与隐式的等待同上text/14-fm-asynchronous-programming.txt:30(搜「explicit」)
回调与嵌套同上text/14-fm-asynchronous-programming.txt:38(搜「callback」) · :59(搜「compareFiles」)
异步会传染同上text/14-fm-asynchronous-programming.txt:69(搜「contagious」)
promise 与 then同上text/14-fm-asynchronous-programming.txt:75(搜「receipt」) · :97(搜「chain」)
randomFile 管道同上text/14-fm-asynchronous-programming.txt:101(搜「randomFile」)
「不管值何时到」同上text/14-fm-asynchronous-programming.txt:120(搜「ignore the question」)
失败通道的规则同上text/14-fm-asynchronous-programming.txt:137(搜「rejected」) · :158(搜「pipeline」)
catch 接住后继续同上text/14-fm-asynchronous-programming.txt:160(搜「Fail」)
Carla 出场同上text/14-fm-asynchronous-programming.txt:176(搜「Berlin」) · :184(搜「Carla」)
密码漏洞同上text/14-fm-asynchronous-programming.txt:192(搜「passcode」)
withTimeout 的一次性同上text/14-fm-asynchronous-programming.txt:198(搜「withTimeout」) · :205(搜「only once」)
递归破译同上text/14-fm-asynchronous-programming.txt:209(搜「recursive」) · :211(搜「crackPasscode」)
555555 与 20 毫秒同上text/14-fm-asynchronous-programming.txt:229(搜「20 milliseconds」) · :232(搜「555555」)
async 重写显形双循环同上text/14-fm-asynchronous-programming.txt:242(搜「pseudosynchronous」) · :265(搜「double loop」)
冻结与唤醒同上text/14-fm-asynchronous-programming.txt:271(搜「frozen」)
生成器与 powers同上text/14-fm-asynchronous-programming.txt:279(搜「generator」) · :281(搜「powers」)
状态自动保存同上text/14-fm-asynchronous-programming.txt:305(搜「automatically save」)
async=特殊生成器同上text/14-fm-asynchronous-programming.txt:309(搜「special type of generator」)
Promise.all 与屏幕墙同上text/14-fm-asynchronous-programming.txt:359(搜「Promise.all」) · :375(搜「VideoPlayer」)
事件循环与空栈同上text/14-fm-asynchronous-programming.txt:423(搜「event loop」) · :412(搜「empty function call stack」)
计时器不准时同上text/14-fm-asynchronous-programming.txt:429(搜「Timeout ran at」)
Me first!同上text/14-fm-asynchronous-programming.txt:439(搜「Me first」)
fileSizes 的缺口同上text/14-fm-asynchronous-programming.txt:451(搜「fileSizes」) · :468(搜「+=」)
修法与「缺口看得见」同上text/14-fm-asynchronous-programming.txt:472(搜「calling join on the result」) · :482(搜「gaps」)

Footnotes

  1. 出处:「Asynchronous Programming」第 18 段(text/14-fm-asynchronous-programming.txt:18,搜「synchronous」)与第 26 段(text/14-fm-asynchronous-programming.txt:26,搜「thread」)。

  2. 出处:「Asynchronous Programming」第 34 段(text/14-fm-asynchronous-programming.txt:34,搜「notoriously hard」)。 2

  3. 出处:「Asynchronous Programming」第 30 段(text/14-fm-asynchronous-programming.txt:30,搜「explicit」)与第 32 段(text/14-fm-asynchronous-programming.txt:32,搜「cuts both ways」)。

  4. 出处:「Asynchronous Programming」第 38 段(text/14-fm-asynchronous-programming.txt:38,搜「callback function」)与第 42 段(text/14-fm-asynchronous-programming.txt:42,搜「setTimeout」)。

  5. 出处:「Asynchronous Programming」第 67 段(text/14-fm-asynchronous-programming.txt:67,搜「indentation level」)与第 69 段(text/14-fm-asynchronous-programming.txt:69,搜「contagious」)。

  6. 出处:「Asynchronous Programming」第 75 段(text/14-fm-asynchronous-programming.txt:75,搜「receipt」)。 2

  7. 出处:「Asynchronous Programming」第 120 段(text/14-fm-asynchronous-programming.txt:120,搜「ignore the question」)。

  8. 出处:「Asynchronous Programming」第 137 段(text/14-fm-asynchronous-programming.txt:137,搜「rejected」)与第 141 段(text/14-fm-asynchronous-programming.txt:141,搜「catch」)。

  9. 出处:「Asynchronous Programming」第 158 段(text/14-fm-asynchronous-programming.txt:158,搜「pipeline」)与第 160 段(text/14-fm-asynchronous-programming.txt:160,搜「Fail」)。

  10. 出处:「Asynchronous Programming」第 192 段(text/14-fm-asynchronous-programming.txt:192,搜「partial code」)与第 194 段(text/14-fm-asynchronous-programming.txt:194,搜「first digit」)。

  11. 出处:「Asynchronous Programming」第 205 段(text/14-fm-asynchronous-programming.txt:205,搜「only once」)。

  12. 出处:「Asynchronous Programming」第 209 段(text/14-fm-asynchronous-programming.txt:209,搜「recursive function」)与第 211 段(text/14-fm-asynchronous-programming.txt:211,搜「crackPasscode」)。

  13. 出处:「Asynchronous Programming」第 238 段(text/14-fm-asynchronous-programming.txt:238,搜「annoying to write」)与第 265 段(text/14-fm-asynchronous-programming.txt:265,搜「double loop」)。

  14. 出处:「Asynchronous Programming」第 269 段(text/14-fm-asynchronous-programming.txt:269,搜「await」)与第 271 段(text/14-fm-asynchronous-programming.txt:271,搜「frozen」)。

  15. 出处:「Asynchronous Programming」第 279 段(text/14-fm-asynchronous-programming.txt:279,搜「generator」)、第 295 段(text/14-fm-asynchronous-programming.txt:295,搜「frozen at its start」)与第 305 段(text/14-fm-asynchronous-programming.txt:305,搜「automatically save」)。

  16. 出处:「Asynchronous Programming」第 307 段(text/14-fm-asynchronous-programming.txt:307,搜「directly in the generator」)与第 309 段(text/14-fm-asynchronous-programming.txt:309,搜「special type of generator」)。

  17. 出处:「Asynchronous Programming」第 359 段(text/14-fm-asynchronous-programming.txt:359,搜「Promise.all」)与第 382 段(text/14-fm-asynchronous-programming.txt:382,搜「play」)、第 404 段(text/14-fm-asynchronous-programming.txt:404,搜「glowing orange bird」)。

  18. 出处:「Asynchronous Programming」第 408 段(text/14-fm-asynchronous-programming.txt:408,搜「run to completion」)与第 423 段(text/14-fm-asynchronous-programming.txt:423,搜「event loop」)。

  19. 出处:「Asynchronous Programming」第 412 段(text/14-fm-asynchronous-programming.txt:412,搜「empty function call stack」)。

  20. 出处:「Asynchronous Programming」第 425 段(text/14-fm-asynchronous-programming.txt:425,搜「dallies」)与第 434 段(text/14-fm-asynchronous-programming.txt:434,搜「Timeout ran at」)。

  21. 出处:「Asynchronous Programming」第 436 段(text/14-fm-asynchronous-programming.txt:436,搜「new event」)与第 439 段(text/14-fm-asynchronous-programming.txt:439,搜「Me first」)。

  22. 出处:「Asynchronous Programming」第 468 段(text/14-fm-asynchronous-programming.txt:468,搜「+=」)与第 470 段(text/14-fm-asynchronous-programming.txt:470,搜「asynchronous gap」)。

  23. 出处:「Asynchronous Programming」第 472 段(text/14-fm-asynchronous-programming.txt:472,搜「calling join on the result」)与第 479 段(text/14-fm-asynchronous-programming.txt:479,搜「Promise.all(lines)」)。

  24. 出处:「Asynchronous Programming」第 482 段(text/14-fm-asynchronous-programming.txt:482,搜「gaps」)。