跳转至

Promise、async/await 与生成器 — 单线程上等一个未来的全部机制

这一章讲三件事: promise——把一个「未来才会发生的结果」变成今天就能传递、 组合、安排后事的对象;async/await——把消费 promise 的链式写法,压回成看起来像同步的代码; 生成器——把函数切成「可以暂停、可以续跑」的片段,以及它和迭代器、异步流水线的关系。 读完你会明白:为什么 promise 的回调总比 setTimeout(..., 0) 的回调先执行——这不是约定,是队列的硬规则。

1. 先看现象:同一个未来,三种写法

一个最简单的未来:「1 秒之后」。最原始的登记方式是把函数交给定时器:

setTimeout(() => { console.log("One second has elapsed"); }, 1_000);

Promise 的写法把「那个未来」先造成一个对象:

const timerPromise = new Promise((resolve) => {
  setTimeout(resolve, 1_000);        // 1 秒后调用 resolve
});
timerPromise.then(() => { console.log("One second has elapsed"); });

好像绕了一圈,但它把未来对象化了,立即换来三件回调做不到的事(原书列举):1

  1. 这个对象可以随便传——任何拿到它的代码都能给自己挂一个 then;
  2. then 可以挂任意多个——同一个 promise,多少回调排队都行;
  3. 可以写组合工具——比如「等这几个 promise 都落定之后」这种高层操作(§4)。

而 await 的写法把消费端再简化一层:

await timerPromise;                  // 代码在这「停」住……
console.log("One second has elapsed"); // ……promise 落定后,从下一行继续

一眼看去这违反了「JavaScript 是单线程」的铁律——代码明明停了 1 秒!其实没有。这一章的旅程, 就是把「没有」两个字讲清楚。

主线例子:一次取数,三次网络故障

为了让每个机制都落在具体数字上,全章跟着一个 fetchJson(url) 走: 向 typicode 测试站取一篇帖子的 JSON。它一路上要闯三关:

  ① 网络本身通不通(断网 → fetch 的 promise 被拒)
  ② 服务器答没答好(状态码非 2xx → 我们主动 throw)
  ③ 正文是不是合法 JSON(response.json() 解析失败 → 被拒)

三个关卡三种失败,却共享同一条错误通道——这就是 promise 的设计精髓。下面逐站走。

2. Promise 的地基:三态、落定,以及 resolve 的怪脾气

2.1 三态与一次性

一个 promise 任何时候必居三态之一:2

  • pending(待定):结果还没来;
  • fulfilled(已兑现):成功落定,带着值;
  • rejected(已拒绝):失败落定,带着原因(reason,惯例是 Error 实例)。

fulfilled 或 rejected 合称 settled(已落定)。最硬的一条规则:

状态只能变一次。 一旦落定,状态和值永久冻结——再调多少次 resolve/reject 都是无效操作。3

2.2 executor:决议权只在内部

构造一个 promise 的形状:

const promise = new Promise((resolve, reject) => {
  // 这坨代码叫 executor(执行器),创建 promise 的当场同步执行
  // 通过 resolve / reject 两个参数掌握这个 promise 的命运
});

注意方向性:promise 对象自己没有任何「改变状态」的方法——决议权完全封在 executor 内部。 改变状态只有三条路:调 resolve、调 reject、executor 里 throw(异常自动等价于 reject)。4

「没有对外改状态的方法」是个刻意的安全设计:状态的改变权只属于造它的人,消费方只能挂回调。 (调试工具能显示状态,那是读的引擎内部接口,不算对外方法。)

2.3 resolve 的怪脾气:遇到 thenable 会「跟着走」

resolve(普通值):promise 以该值兑现——这半句好懂。麻烦在另一半:5

如果 resolve 收到的是一个带 then 方法的对象(术语 thenable), 这个 promise 不会立刻兑现,而是「订阅」那个 then,把对方的状态当作自己的。

promise 自己就有 then 方法,所以每个 promise 都是 thenable——推论:

const p1 = new Promise((resolve) => resolve(fulfilledPromise));
//  p1 不直接兑现;它「跟随」fulfilledPromise,对方兑现什么值,它就是什么值
const p2 = new Promise((resolve) => resolve(rejectedPromise));
//  p2 跟着 rejectedPromise 一起被拒

带着数走一遍书里的例子:fulfilledPromise = Promise.resolve("fulfilled"), p1 = new Promise(r => r(fulfilledPromise))——r 收到的是个 thenable,p1 订阅它, fulfilledPromise 兑现 "fulfilled" 时,p1 也兑现 "fulfilled"。6

更极端的:thenable 的 then 如果永远不调任何回调,这个 promise 永远 pending—— resolve 了却没落定,这是新手最常栽的「promise 卡死」之一。

resolved ≠ settled(规范术语,原书特意分清):7

  • resolved = resolve/reject 已经被调过(再调无效);
  • settled = 状态进了 fulfilled 或 rejected。

一个 resolve 了 thenable 的 promise 是 resolved 但未 settled——它还在等别人。 日常口语里「resolved」常被当「fulfilled」用,规范里不是。

补充一个 ES2024 的方便货:Promise.withResolvers(),一次返回 { promise, resolve, reject }——把决议函数掏出来带走的官方姿势, 以前要靠在 executor 里给外层变量赋值的 hack。8

3. then 的链式魔法:新 promise、错误传播、一个 catch 守三关

then(onFulfilled, onRejected) 的完整签名里藏了全章最重要的机制:

then 自己返回一个新的 promise。9

新 promise 的命运由回调的行为决定:

回调干了什么 then 返回的新 promise
return 一个值 以该值兑现(走同一套 resolve 规则,return thenable 会跟随)
throw 一个错 以该错误被拒
return 一个 promise 变成那个 promise 的「拷贝」

错误沿链传播的机制由此浮出:任何一环的回调 throw,下一环的新 promise 就被拒, 再下一环还是新的拒绝……一路滚到链尾。而 catch(onRejected) 只是 then(undefined, onRejected) 的简写——所以一个 catch 能接住链上任何一环抛出的错。10

主线例子走完整链(书里的 fetch 流程,带三关):

fetch("https://jsonplaceholder.typicode.com/posts/1")     // ① 关卡:网络
  .then((response) => {
    if (!response.ok) {                                    // ② 关卡:状态码
      throw new Error(`Status code ${response.status}`);   //    throw → 链进入拒绝
    }
    return response.json();                                // ③ 关卡:解析;返回 promise → 链跟随
  })
  .then((json) => { console.log(json); })                 // 拿到解析好的帖子
  .catch((reason) => { console.error(`Request failed: ${reason}`); })
  .finally(() => { console.log("Request finished"); });   // 落定必跑,fulfilled/rejected 都跑

三关对号:断网 → fetch 的 promise 被拒 → 一路滚到 catch; 状态码 500 → 第二环 throw → 滚到 catch;正文非法 JSON → response.json() 的 promise 被拒 → 滚到 catch。 一个 catch,三种来源,全部兜住。 这是 promise 相对回调地狱的第一个质变:错误有了统一的通道。

finally(ES2018)补上了 try/catch/finally 的类比——它是「无论成败都要跑」的那一段 (清理工作),也是 then 的语法糖,不改动链上传递的值或错误。11

原书在这里记了两个工程教训:其一,promise 最大的坑是错误默认被静默吞掉——只挂 onFulfilled 忘了挂拒绝处理,拒绝就悄悄消失(typescript-eslint 的 no-floating-promises 规则专治此症); 其二,then/catch/finally 都改不了它们所挂的 promise——链上的每个环都是新对象, 能改状态的只有 resolve/reject。12

4. 多个 promise 的四种组合

并发的场景:渲染一个界面前要同时取三份数据。该同时发(不该串行等), Promise.all 就是干这个的:13

Promise.all([p1, p2, p3].map(...))   // 输入 promise 数组
// 全部兑现 → 新 promise 兑现为「值数组」,顺序与输入一一对应
// 任何一个被拒 → 新 promise 立刻以该原因被拒

「全好才算好、一坏全坏」——Promise.all 是典型的 all-or-nothing 工具, 也是实践中最常用的并发组合。顺序保证值得单独记:输出数组的第 i 项就是第 i 个输入的兑现值, 与谁先谁后完成无关。14

四个静态方法的取舍(原书给了完整对照的散件,我们合表):

方法 何时落定 失败语义
Promise.all 全部兑现 任一拒绝 ⇒ 立即拒绝(全或无)
Promise.allSettled(ES2020) 全部落定(含拒绝) 永不拒绝;结果数组带每项的 status 和 value/reason
Promise.any(ES2021) 第一个兑现 全部拒绝 ⇒ 以「原因数组」拒绝
Promise.race 第一个落定(好坏都算) 第一个落定的是拒绝 ⇒ 拒绝

race 的经典用法是给另一个 promise 掐表(书里的例子):15

const fetchController = new AbortController();
const fetchPromise = fetch("https://httpbin.org/delay/2", { signal: fetchController.signal })
  .then((r) => { if (!r.ok) throw new Error(`Status code ${r.status}`); return r.json(); });

const timeoutPromise = new Promise((_, reject) =>
  setTimeout(() => { fetchController.abort(); reject(new Error("Request timeout")); }, 1_000)
);

Promise.race([fetchPromise, timeoutPromise])
  .then((result) => console.log(result))
  .catch((reason) => console.error(`Request failed: ${reason}`));

带数走:目标站 2 秒才响应,超时 1 秒先响——race 以 timeoutPromise 的拒绝落定, catch 打出 "Request timeout"。注意 AbortController(ES2018):它是 promise 时代「取消」问题的官方答案, signal 塞进 fetch 的选项,abort() 一调,底层请求真的被撤销——没有它,超时的请求还会在后台继续跑, 白白占着连接。原书称其为「被低估的特性」。16

any 的典型场景:「数据库和缓存同时查,缓存成了就行」;allSettled 的典型场景: 「全都要结果,失败的要失败详情」——书里还给了两者组合使用的思路(先 all 报总错、再 allSettled 补细节)。17

一个共同的天花板(原书边栏):这四个方法都只产出一个最终 promise,管不了 「有界并发」——比如 500 个任务只想同时跑 8 个。那要用 p-limit 这类库(Sindre Sorhus 家的一族: p-limit、p-throttle、p-debounce)。18

5. 微任务与宏任务:先讲纪律,再看 await 怎么服从它

5.1 两个队列,一条优先级

第 4 章说事件进队列;本章把「队列」这个词拆开——其实是两条:19

  • 微任务队列(microtask queue),规范名 job queue:then/catch/finally 的回调进这里;
  • 宏任务队列(macrotask queue),也叫 task queue:setTimeout、DOM 事件、I/O 回调进这里。

调度规则只有三条,背下来:

  1. 跑当前的代码,直到自然结束
  2. 微任务队列非空 → 取一个微任务跑(反复直到清空)
  3. 宏任务队列非空 → 取一个宏任务跑,然后回到 2

微任务严格优先。 于是书上那个著名实验的输出毫无悬念:20

setTimeout(() => { console.log("setTimeout callback"); }, 0);
Promise.resolve(true).then(() => { console.log("then callback"); });
console.log("end of script");
  end of script        ← 同步代码先跑完(规则 1)
  then callback        ← 微任务清空(规则 2)
  setTimeout callback  ← 才轮到宏任务(规则 3)

两个细节一起解释:其一,then 绝不同步调用回调——哪怕 promise 早已落定, 回调也要排到微任务里。这是 blessing:then 之后的代码永远先于回调执行,控制流可预测。21 其二,setTimeout(..., 0) 名不副实:HTML Living Standard 规定它的最小延迟是 4 毫秒; 但哪怕真是 0,then 也赢——赢在队列,不赢在时间。22

分属两份规范的纪律(书里指出的精微处):promise 与微任务的规则在 ECMAScript 规范里 (引擎必须遵守);setTimeout/queueMicrotask 的定时规则在 HTML Living Standard 里 (非浏览器运行时本来可以不同——Node 开了「跟随 HTML 规范」的先例,大家都跟了)。23

queueMicrotask 是把代码排进微任务的官方 API(以前是「resolved 的 promise 的 then」的歪招); React 批量更新 DOM 这类「要比 setTimeout 更快的异步」用的就是它。24

5.2 await 的真身:挂起 + 微任务续跑

语法上 await 值 做两件事:把值按 resolve 的同一套规则转成 promise(等价 Promise.resolve(值)), 然后把当前函数挂起,直到 promise 落定。25

「挂起」不是阻塞——挂起的瞬间,函数把控制权交还事件循环,其他事件照常处理(这正是 §1 谜底的一半)。 promise 一落定,恢复的方式就是排一个微任务:

  • 落定为 fulfilled:恢复执行,await 表达式取兑现值;
  • 落定为 rejected:在 await 那一行抛出拒绝原因。

三个微型的行为样例(原书的):

console.assert((await "Phantom Blade") === "Phantom Blade");            // 非 promise 值:原样返回
console.assert((await Promise.resolve("Falling Devil")) === "Falling Devil");
await Promise.reject(new Error("Deadly Mirage"));                       // 在 await 行 throw

「在 await 行 throw」这一条是 await 相对 then 的隐藏升级:拒绝变成了带完整调用栈的普通异常, try/catch 直接接——promise 时代早期最大的抱怨(错误没有栈、不知哪里抛的)就此了结。26

那「为什么允许 await 的地方这么少」也有了答案:await 只允许出现在两个位置—— ECMAScript 模块的顶层、用 async 声明的函数体内。原因正是 §1 的另一半谜底: 如果任何函数都能 await,就意味着任何函数都可能「在半中间暂停、把控制权交出去」—— 调用方「函数体一鼓作气跑完」的古老假设将全军覆没。async 函数的保证 (总返回 promise)把「暂停」关进了 promise 的笼子里:调用方看到的只是一次普通的异步返回。27

.mts 后缀(第 3 章见过)在这里派上用场:它是 ESM,所以模块顶层随便 await。28

5.3 async:保证「产出 promise」

async 只出现在函数定义处(function 或箭头函数前),效果两条:29

  • return 的值按 resolve 规则包成 promise(已是 promise 则原样);
  • throw 的值变成拒绝的 promise(throw error ≡ return Promise.reject(error))。

于是「调用 async 函数永远拿到 promise」成为类型层的保证: TypeScript 里 makeNinjaName 的返回类型显示为 Promise<string>。 (拒绝原因的类型总是 unknown——和 catch 到的错误一样,这是检查器承认「拒绝可以抛任何东西」。)

主线例子的最终形态,两个关键字合体:

const fetchJson = async (url: string) => {
  const response = await fetch(url);                 // 挂起,网络回来续跑
  if (!response.ok) {                                // 状态码关卡
    throw new Error(`Status code ${response.status}`);
  }
  return response.json();                            // 返回 promise:async 会跟随
};

try {
  console.log(await fetchJson("jsonplaceholder.typicode.com/posts/1"));
} catch (error) {                                    // 三个关卡的所有错误,一个 catch
  console.error(`Request failed: ${error}`);
} finally {
  console.log("Request finished");
}

对比 §3 的链式版本:逻辑顺序=书写顺序,错误处理=try/catch/finally——异步代码终于有了 同步代码的全部 ergonomics(人机工学)。书里记了一句时代观察:API 设计因此整体从 「接受回调」转向「返回 promise」——Node 的 fs 模块老接口之外有了 fs/promises, 还有 util.promisify 把任何老式回调函数包成返回 promise 的函数。30

一条 lint 纪律:promise-function-async 规则建议「凡返回 promise 的函数都标 async」, 动机是错误处理的一致性——调用方期望「错误从 promise 里来」,而不是函数同步 throw。31

6. 生成器:把函数切成可恢复的片段

6.1 来龙去脉:期望落空的小历史

生成器和 promise 同在 ES2015 登场,当年被期待组成「消灭回调地狱的双人组」。 结果没成:语法怪(function*),更关键的是 ES2017 的 async/await 拿走了它的主战场。 今天生成器被视为「小众但有用」的工具——这是原书给的诚实定位。32

理解它的钥匙是上一句话的反面:async 是「定义返回 promise 的函数的语法糖」, 生成器是「定义返回迭代器的函数的语法糖」。 所以要先讲迭代器。

6.2 迭代器协议:for...of 底下的那套约定

ES2015 的 for...of 不只服务数组,它能遍历任何满足两条约定的对象:33

  1. 对象上有一个键叫 Symbol.iterator(Symbol 是 ES2015 引入的「唯一非字符串的键类型」, Symbol.iterator 是 Symbol 类上的预置静态属性之一);
  2. 那个键对应的值是个函数,调用它返回一个带 next() 方法的对象;每次调 next(), 要返回一个 { value, done } 形状的对象。

满足这两条的对象叫 iterable(可迭代对象);next() 所在的对象叫 iterator(迭代器)。 for...of 做的事,本质就是反复调 next() 直到 done 为 true。

书里把数组的迭代器掏出来手动走了一遍(「非常啰嗦,但展示了 for...of 底下没什么魔法」):34

const numbers = [1, 2, 3];
const iterator = numbers[Symbol.iterator]();
let r = iterator.next();                    // { value: 1, done: false }
while (!r.done) { console.log(r.value); r = iterator.next(); }
// 1  2  3,最后 r = { value: undefined, done: true } 循环结束

迭代协议还藏在两个熟面孔底下:解构也是迭代——const [a, b, c] = ["a","b","c"] 其实是 「调迭代器取前三个值」;展开/剩余也是迭代——func(...iterable)、[...iterable]、 const [first, ...others] = iterable。35

一个自造 iterable 的秀肌肉例子(书里的):无限随机数流——36

const randomNumbers: Iterable<number> = {
  [Symbol.iterator]: () => ({
    next: () => ({ value: Math.random() }),   // 永远没有 done: true
  }),
};
const [num1, num2] = randomNumbers;   // 解构取前两个,优雅停下
console.log(num1, num2);

迭代器可以是无限的、可以惰性产生值(要一个产一个)——这是数组给不了的灵活性, 也是生成器登场的理由。

6.3 生成器:yield 是「暂停并吐出一个值」

生成器函数的定义: function 后加 *(箭头函数不行)。它一被调用,不执行函数体, 而是返回一个生成器对象——这个对象既是 iterable 又是 iterator(调它自己的 [Symbol.iterator]() 返回它自己)。37

  function* randomNumbers() {        // 生成器函数
    while (true) { yield Math.random(); }   // yield:暂停在这一行,把值递出去
  }
  const g = randomNumbers();         // 函数体一行没跑,拿到生成器对象
  const [num1, num2] = g;            // 迭代它:两次 next(),取两个随机数

next() 和 yield 的精确协议(全章最该背的一段):38

  • next() 一调,函数体从上次停的地方(或开头)跑到下一个 yield; yield 后表达式的值成为本次的 { value, done: false };
  • 函数return(或自然走到末尾):{ value, done: true };
  • 函数 throw:异常从 next() 调用处抛出。

「跑到 yield 就暂停、保持全部局部状态」是它和普通函数的本质区别——await 暂停整个线程 (让事件循环走),yield 只暂停自己(控制权立刻回到调 next() 的地方,就像 return 一样), 下次 next() 从断点续跑。39

双向通道(较少用但书里给了完整例子):next(值) 可以把值塞回生成器—— 恢复执行时,那个 yield 表达式的值就是你塞进去的值:40

function* multiplier(): Generator<number, unknown, number> {
  let number = 1;
  while (true) {
    const multiplier = yield number;   // 先吐出 number,再把 next(传入值)交给 multiplier
    number *= multiplier;
  }
}
const g = multiplier();
g.next().value === 1;    // 吐出 1
g.next(5).value === 5;   // 1×5=5
g.next(4).value === 20;  // 5×4=20

yield* 把迭代委托给另一个生成器(书里的武士名单例子):41

function* warriors() {
  yield "Sun Tzu";
  yield* ninjas();        // 先把 ninjas 的全部 yield 走完
  yield "Genghis Khan";
}
function* ninjas() { yield "Hattori"; yield "Yoshi"; }
[...warriors()];   // ["Sun Tzu", "Hattori", "Yoshi", "Genghis Khan"]

yield* 的杀手场景是递归:一个生成器可以把「同一生成器函数的另一份调用」委托下去—— 于是「遍历一棵 DOM 树」可以写成一个朴素的 for...of,而不必先摊平成数组。42

6.4 异步生成器:for await...of 与流

普通生成器的 yield 不会暂停线程——这对「迭代下一个值」是对的语义,但遇上异步数据源就不够了。 ES2018 的答案:async 生成器(async function*),配 for await...of 消费。43

协议只换一处:异步迭代器的 next() 返回 promise(落定值为 { value, done }), 键从 Symbol.iterator 换成 Symbol.asyncIterator。但写代码时这些都被语法藏起来了:

async function* fetchPosts() {
  for (let i = 1; i <= 3; i++) {
    const response = await fetch(`jsonplaceholder.typicode.com/posts/${i}`);
    if (!response.ok) throw new Error(`Status code ${response.status}`);
    yield await response.json();        // await 和 yield 终于合体:这才是「真暂停」
  }
}
for await (const post of fetchPosts()) { console.log(post); }

书里的点睛句:「结合了 yield 与 async 和 await,我们终于有了一种在生成器函数内部真正暂停代码执行的能力。」 每循环一次:取第 i 篇 → 等网络 → 解析 → 吐出——三件事按书写顺序发生,全部是非阻塞的等待。44

消费端的不对称要记:异步迭代器没有解构、不能展开传参,唯一的语法就是 for await...of (以及直接调 next())。45

现实坐标:流式 API(ReadableStream)也是异步可迭代对象——「和网页版大模型对话时, 文字一段段往外冒」很可能就是一个 for await...of 在渲染流(chunk)。原书把这条线指到了第 11 章(streams,MEAP 尚未出)。46

什么时候该写生成器(原书的收尾判断):当你想把「产生值的过程」包成 iterable 让别人好消费时—— 解构赋值、递归遍历树的 for...of、逐个处理昂贵任务的 for await...of,都是它的甜区。 「下次写着复杂循环时,问自己:写成生成器会不会更优雅。」47

7. 作者的判断与证据

可亲手验证的机制:

  • 三态/一次性/thenable 跟随,全部有 assert 级示例(如 promise1/promise2 的输出文本);
  • 微任务实验的输出顺序(end of script → then callback → setTimeout callback)在任何环境稳定复现;
  • multiplier 生成器的 1、5、20 三个断言是逐值可验的。48

作者立场(分开读):

  • 「生成器期望落空、如今小众」是行业观察,但「yield 不适合暂停线程」的机制论证是硬事实;
  • 「AbortController 被低估」是作者的评价,它确实是取消问题的标准答案;
  • 「then 回调永不同步」被称 blessing——对可预测性是 blessing,对「以为同步」的新手是坑的两面。

8. 边界与局限

  • thenable 的识别靠「有没有 then 方法」,鸭子类型的双刃剑:普通对象手滑加个 then 属性, resolve 它就会被当成 thenable。书里没展开这个陷阱,但机制解释足够推出它。
  • Promise 组合方法不管有界并发——p-limit 这类库不在书内(只给了名字和链接)。
  • async 生成器错误处理(生成器内部 throw 在 for await 里的表现)书中未细讲。
  • MEAP 缺口: streams(第 11 章)与模块(第 13 章)都被引用但不存在; 取消(cancellation)的完整讨论要等后续版本。

9. 可带走的(合上这一章,你应该有这十二条)

  1. promise 是「一次未来」的对象化:能传、能挂多个 then、能组合——回调给不了的三件事。
  2. 三态 + 落定后永久冻结;状态改变权封在 executor 里,消费方只有 then/catch/finally。
  3. resolve(thenable) 会跟随对方——所以 resolved ≠ settled,卡死的 promise 多半是这么来的。
  4. then 永远返回新 promise:回调 return 值→兑现、throw→拒绝、return promise→跟随——链式与错误传播都由此而来。
  5. 一个 catch 守多关:链上任何一环的拒绝/throw 都滚到链尾,这是 promise 对回调的第一个质变。
  6. all(全好才算好)、allSettled(全都要结果)、any(谁先成算谁)、race(谁先落定算谁);race+AbortController 是掐表的标准姿势。
  7. 微任务严格优先于宏任务——then 总比 setTimeout 先跑;then 绝不同步调回调。
  8. await = 转成 promise + 挂起函数 + 落定后排队恢复(fulfilled→取值;rejected→在 await 行 throw,带完整栈)。
  9. await 只许在两个地方:模块顶层、async 函数体内——这是「函数可能中途让权」的隔离带。
  10. async 保证函数总返回 promise:return 包一层、throw 变拒绝;Promise<string> 是类型层的承诺。
  11. 生成器 = 返回迭代器的语法糖:next() 跑到下个 yield 就暂停并保持状态;yield* 委托,天然适合递归遍历。
  12. async 生成器 + for await...of 是唯一「真暂停」的迭代——流式数据(大模型打字机效果)的标准消费姿势。

11. 深读一:决议算法的完整规则书

第 2 节把 resolve 的三条路径和 thenable 讲了,这里把「一个 promise 的一生」 整理成一份可查阅的规则书——每一条都对应书里的一个断言或可观察行为。

  创建:new Promise(executor)
      └─ executor 同步执行;它手里握着这个 promise 的全部命运

  落定(三选一,先发生者赢,之后的一切调用无效):
      ① resolve(普通值)      → fulfilled,值 = 该值
      ② reject(原因)          → rejected,原因 = 该值(惯例是 Error 实例)
      ③ executor 里 throw     → rejected,原因 = 被抛出的值

  resolve(thenable) 的跟随规则:
      └─ 订阅 thenable 的 then;对方兑现什么/因何拒绝,本 promise 照抄
      └─ 对方永远 pending,本 promise 也永远 pending(已 resolved 但未 settled)

  消费:then(onFulfilled, onRejected)
      └─ 回调永不同步执行,一律进微任务队列
      └─ then 返回【新】promise:回调 return 值→新 promise 兑现(同样走跟随规则)
                                  回调 throw  → 新 promise 拒绝

三条最容易栽的规则,各配一个书里的实证:1

// 实证①:resolve 不保证落定
new Promise((resolve) => resolve({ then: () => {} }));   // 永远 pending

// 实证②:resolve(另一个 promise) = 跟随
const p1 = new Promise((r) => r(Promise.resolve("fulfilled")));
// p1 不直接兑现;等内层兑现 "fulfilled",p1 才兑现同一个值

// 实证③:thenable 判定看「有没有 then 方法」
// ——所以普通对象手滑加了个 then 属性,resolve 它就会把它当 thenable(跟随的陷阱)

「决议函数可以逃出 executor」这个老 hack 也值得一记(书里给了): 在 executor 里把 resolve/reject 赋给外层变量,别处就能调用—— ES2024 的 Promise.withResolvers() 就是把这个 hack 收编为正式 API: const { promise, resolve, reject } = Promise.withResolvers();。 它是「回调式 API 与 promise 的桥」:把 promise 的 resolve 当回调传给老式接口, 一次调用就把那个老接口 future 化了。2

12. 深读二:错误处理的工程面

12.1 一个 catch 管三关的完整账

第 3 节的 fetch 链,把错误通道的「汇总能力」拆开记账:

  fetch(url) ──可能断网──────► 它的 promise 被拒 ──┐
      .then(检查 status) ──主动 throw 错误 ────────┤ 三条支流
      .then(解析 JSON) ──正文不合法───────────────┘
                        │
                        ▼ 都汇入同一个拒绝通道
                    .catch(reason) ──一次处理

这个设计在回调时代不可想象:嵌套回调里,每个分支都要自己传错误、自己决定要不要继续传。 promise 把「错误的流向」从每个回调的私事,变成链的公共设施—— catch 之于 promise 链,正如 catch 之于 try/catch(方法名都不是巧合)。 finally(ES2018)补的是第三块拼图:无论成败都要执行的收尾(关 loading、记日志), 它不接收值也不改链上的值。3

12.2 静默失败:被低估的头号坑

书里对 promise 的抱怨排了名,第一名叫「错误默认被静默忽略」:

fetchJson("/api/data").then((data) => render(data));
// 若 promise 被拒绝,且链上没有 onRejected——拒绝悄悄消失,控制台可能只有一行系统警告

对策是 lint 层的:typescript-eslint 的 no-floating-promises 规则强制要求 每个 promise 的拒绝都被显式处理(或显式声明「我不处理」)。 一条规则补上语言设计里没做的那半件事。4

12.3 组合四方法的选型决策树

  多个 promise,我想要……
      │
      ├─ 全部成功才算成功,一个坏全完     → Promise.all
      ├─ 每个的结果(成败都要)都拿到      → Promise.allSettled
      ├─ 谁先成功算谁赢(备胎模式)        → Promise.any
      └─ 谁先有结果算谁(含失败)/掐超时   → Promise.race

all 的顺序保证值得单独再记一次:输出数组的第 i 项 = 输入第 i 个 promise 的兑现值, 与完成先后无关——所以你可以放心地把「请求列表」和「结果列表」按下标对起来。5

race 掐超时的完整剧本(书里的 Listing 8.5)再压成一张时刻表:

  0ms   发请求(httpbin 的 /delay/2,2 秒后才响应);同时起 1 秒超时的定时器
  1000ms 超时先到:timeoutPromise 拒绝 → race 落定(拒绝) → catch 打出 "Request timeout"
        且 AbortController.abort() 撤销了底层请求——不占连接
  2000ms 服务器就算后来响应了,也没人听(fetch 已被撤销)

AbortController 的价值在第二行:没有它,「超时」只是「不等了」, 请求还在后台烧资源;有了它,超时是「真的取消」。书里的评语:「underused feature」。6

12.4 有界并发:组合方法够不着的那块

all/any/race/allSettled 有个共同的天花板(书里立框指出):它们只返回一个 promise, 管的是「最终事件」或「首个事件」,管不了「同时最多跑几个」。 场景:500 个任务想限制到同时 8 个(比如开 Web Worker 得省着内存)。 答案是库:Sindre Sorhus 的 p-limit(配任何返回 promise 的函数用), 它所属的 p-* 轻量库家族还有 p-throttle(限流)、p-debounce(防抖)。7

13. 深读三:微任务纪律的完整版

13.1 两条队列的「优先级」究竟是什么意思

「微任务严格优先」要读成一条循环纪律,而不是「微任务先排队」:

  跑当前代码(直到自然结束)
      ↓
  清空微任务队列(全部跑完,新来的微任务也立刻跑)
      ↓
  取一个宏任务执行
      ↓
  回到「清空微任务队列」

「清空」两个字是关键:一个微任务里如果又排了微任务(then 回调里再 .then), 它也在本轮执行——宏任务得等整列微任务排空。这就是为什么 Promise.resolve().then(() => Promise.resolve().then(() => console.log("深层"))) 的每一层都跑在任何 setTimeout 之前。8

实证(书里的 Listing 8.6)再贴一次,它值得背下来:

setTimeout(() => { console.log("setTimeout callback"); }, 0);
Promise.resolve(true).then(() => { console.log("then callback"); });
console.log("end of script");
// end of script → then callback → setTimeout callback

三个层次一次看全:同步代码最先(规则一);promise 已落定,then 的回调仍要排队(规则二—— 「promises never call the then callback synchronously」是刻意的,它让 then 之后的代码 永远先于回调执行,控制流可预测);setTimeout(...,0) 名不副实——HTML 标准规定它最小延迟 4 毫秒,但哪怕真是 0,它也在宏任务列队,输的是队列不是时间。9

13.2 规范的接缝:谁管什么

书里指了一个精微处:promise 和微任务的时序规则在 ECMAScript 规范里 (引擎必须遵守);而 setTimeout、queueMicrotask 的定时规则在 HTML Living Standard里 ——理论上非浏览器运行时可以给它们不同行为。 现实是 Node 开创了「跟随 HTML 标准」的先例,其他运行时跟进。 这条接缝解释了为什么「setTimeout 最小 4ms」是 HTML 标准的事,而「then 永不同步」 是 ECMAScript 标准的事——两个承诺来自两份文件。10

queueMicrotask 是把代码排进微任务的官方 API——在它有之前, 「想比 setTimeout 更快的异步」只能借 Promise.resolve().then() 的壳; React 批量更新 DOM 用的就是微任务通道(渲染虽不由 JS 做,浏览器把渲染排在宏任务侧)。 知道这条通道的存在,你也能写「尽快但非同步」的代码了。11

14. 深读四:async/await 的完整语义与类型面

14.1 await 的两步,逐步验证书里的三个样例

await 值的精确语义(书里给的两步):先按 resolve 的同一套规则把值转成 promise (等价 Promise.resolve(值)),再把函数挂起等它落定。三个样例逐个验算:

console.assert((await "Phantom Blade") === "Phantom Blade");
// 字符串不是 thenable → Promise.resolve 原样兑现 → await 原样返回

console.assert((await Promise.resolve("Falling Devil")) === "Falling Devil");
// 已是 promise → 原样等待 → 返回兑现值

await Promise.reject(new Error("Deadly Mirage"));   // throws
// 拒绝 → 在 await 那一行抛出原因——带完整调用栈的普通异常

第三点是 await 相对 then 的隐藏升级:拒绝变成了带完整栈的 throw, try/catch/finally 直接通用。promise 时代「错误没有栈、不知出处」的最大抱怨就此了结。12

挂起的函数并没有阻塞线程——「JavaScript 引擎将走到事件循环的下一步」, 其他事件照常触发。await 的隔离带设计也由此说透:它只允许出现在 模块顶层或 async 函数体内。若任何函数都能 await, 「调用一个函数会一鼓作气跑完」的古老假设就废了; async 函数的总返回 promise 把这个「可能暂停」包成了调用方看得见、管得着的形状。13

14.2 async 的两条保证,与「类型层的承诺」

async 函数(function 或箭头函数前加 async)的效果两条(书里照录):14

  • return 的值按 resolve 规则包成 promise(已是 promise 则原样)—— return value ≡ return Promise.resolve(value);
  • throw 变成拒绝——throw error ≡ return Promise.reject(error)。

由此,「调用 async 函数永远拿到 promise」成了类型层的定理: TypeScript 里 makeNinjaName 的返回类型显示为 Promise<string>。 同时规定了一个不对称:promise 的拒绝原因类型总是 unknown (和 catch 到的错误一样——「拒绝可以抛任何东西」是检查器承认的现实, 最佳实践是只 reject Error,但语言不强制)。

fetchJson 终态(书里的 Listing 8.8)把两条关键字合在一个函数里, 它值得当模板背:

const fetchJson = async (url: string) => {
  const response = await fetch(url);
  if (!response.ok) {                       // 状态码关卡:主动 throw
    throw new Error(`Status code ${response.status}`);
  }
  return response.json();                   // 返回 promise:async 会跟随
};

try {
  console.log(await fetchJson("jsonplaceholder.typicode.com/posts/1"));
} catch (error) {                           // 三关的所有错误,一个 catch
  console.error(`Request failed: ${error}`);
} finally {
  console.log("Request finished");
}

API 生态随之转向(书里的时代观察):Node 给经典回调式 fs 之外加了 fs/promises; util.promisify 能把任何老式回调函数包成返回 promise 的版本。 「返回 promise」从库的设计风格,变成了接口的默认期待。15

一条 lint 纪律顺带记住:promise-function-async 规则要求「凡返回 promise 的函数 都标 async」,动机是错误处理的一致性——调用方期待错误从 promise 里来, 而不是函数同步 throw(那种错误 try/catch 接不住、调用方也没准备)。16

15. 深读五:迭代器协议的完整解剖

15.1 两条约定,逐个符号看清

for...of 能遍历的任何东西,满足两条约定(书里写得很正式):17

  约定一:对象上有一个键 Symbol.iterator
          Symbol 是 ES2015 引入的「唯一非字符串键类型」——每个 Symbol 值都独一无二,
          Symbol.iterator 是 Symbol 类预置的静态属性之一
  约定二:object[Symbol.iterator] 是一个函数,
          调用它返回一个带 next() 方法的对象;
          每次调 next() 要返回 { value, done } 形状的对象

满足两者的对象叫 iterable;next() 所在的对象叫 iterator。 for...of 的本质就是:反复调 next(),直到 done 为 true—— 把迭代器手动走一遍(书里的 Listing 8.9),等价过程一览无余:

const numbers = [1, 2, 3];
const iterator = numbers[Symbol.iterator]();
let r = iterator.next();            // { value: 1, done: false }
while (!r.done) { console.log(r.value); r = iterator.next(); }
// 打 1、2、3;最后一次 r = { value: undefined, done: true }

「数组没什么魔法」是这一节的精神:iterable 是可以自己写的协议, 数组只是内置的一份实现。18

15.2 迭代协议的三个隐藏用户

第 8 章前面埋的「解构/展开也是迭代」值得展开成清单——同一个协议,三处语法共用:

const [a, b, c] = ["a", "b", "c"];   // 数组解构 = 调迭代器取前三个值
func(...iterable);                   // 展开传参 = 把迭代器依次喂给形参
const array = [...iterable];         // 展开成数组 = 收集全部迭代值
const [first, ...others] = iterable; // 剩余语法 = 取一个、收剩下的

警告也在书里:把无限 iterable 整个展开,程序就悬了—— [...randomNumbers] 永远跑不完。迭代的「惰性」是优点也是责任。19

自造 iterable 的秀肌肉例子(Listing 8.10)再读一遍:

const randomNumbers: Iterable<number> = {
  [Symbol.iterator]: () => ({
    next: () => ({ value: Math.random() }),   // 没有 done——永不结束
  }),
};
const [num1, num2] = randomNumbers;   // 解构取两个就优雅停下

iterable 不必是有限的、静态的集合——它可以是「要一个产一个」的懒生产者; next() 里可以干任何事。生成器要解决的,正是「手写这个协议太啰嗦」的问题。20

16. 深读六:生成器的完整协议与三种高阶用法

16.1 next/yield 的往返协议

生成器函数(function* 定义;箭头函数不行)一调用不执行函数体, 返回一个生成器对象——它既是 iterable 又是 iterator (generator[Symbol.iterator]() 返回它自己)。next() 与 yield 的往返规则 (书里列了三条,照录):21

  next() 一调:函数从上次停点跑到下一个 yield
      → 本次返回 { value: yield 后面的值, done: false }
  函数 return(或自然走到末尾)
      → { value: return 后面的值, done: true }
  函数 throw
      → 异常从 next() 调用处抛出

和 await 的对照再明确一次:await 暂停整个线程(让事件循环走), yield 只暂停自己(控制权立刻回到调 next() 的地方,像 return 一样), 下次 next() 从断点续跑,局部状态原样还在。22

无限随机数生成器(Listing 8.11)是协议的最小演示:

function* randomNumbers() {
  while (true) { yield Math.random(); }
}
const g = randomNumbers();        // 函数体一行没跑
const [num1, num2] = g;           // 迭代两次:取两个随机数

16.2 双向通道:multiplier 的逐步验算

next(值) 能把值塞回生成器——恢复执行时,那个 yield 表达式的值就是塞进去的值。 书里的 multiplier(Listing 8.12),逐步验算:

function* multiplier(): Generator<number, unknown, number> {
  let number = 1;
  while (true) {
    const multiplier = yield number;   // 吐出 number,然后把 next(塞入值)交给 multiplier
    number *= multiplier;
  }
}
const g = multiplier();
g.next().value === 1;    // 首次:无塞入值,yield 1;number 还是 1
g.next(5).value === 5;   // 塞入 5:1×5=5;yield 5
g.next(4).value === 20;  // 塞入 4:5×4=20;yield 20

三步各对上书里的一条 assert。这是生成器最少用、但最能说明「yield 是双向路口」的例子; 日常代码里多数生成器只用单向(吐值)那一半。23

16.3 yield* 委托与递归遍历

yield* another() 把迭代委托给另一个生成器(Listing 8.13):

function* warriors() {
  yield "Sun Tzu";
  yield* ninjas();        // 先把 ninjas 的全部 yield 走完
  yield "Genghis Khan";
}
function* ninjas() { yield "Hattori"; yield "Yoshi"; }
[...warriors()];   // ["Sun Tzu", "Hattori", "Yoshi", "Genghis Khan"]

委托的用武之地是递归:一个生成器可以把「同一生成器函数的另一份调用」 yield* 下去——「遍历一棵 DOM 树」因此可以写成一个朴素的 for...of, 而不必先把树摊平成数组(摊平既费内存又丢结构)。24

16.4 异步生成器与 for await...of

普通生成器的 yield 不暂停线程——这对「迭代下一个值」是对的语义, 遇上异步数据源就不够了。ES2018 的补全:async 生成器 + 异步迭代协议。 协议的差别只有一处(书里点得清楚):异步迭代器的 next() 返回 promise (落定值是 { value, done }),键从 Symbol.iterator 换成 Symbol.asyncIterator。 但写代码时这些都被语法藏起来了——async function* 定义、for await...of 消费:

async function* fetchPosts() {
  for (let i = 1; i <= 3; i++) {
    const response = await fetch(`jsonplaceholder.typicode.com/posts/${i}`);
    if (!response.ok) throw new Error(`Status code ${response.status}`);
    yield await response.json();   // await 与 yield 合体:真·暂停
  }
}
for await (const post of fetchPosts()) { console.log(post); }

循环一次的完整时间线:取第 i 篇 → 挂起等网络 → 解析 → 吐出—— 三件事按书写顺序发生,等待期间线程照常处理别的事件。 书里的点睛句照录:「结合了 yield 与 async 和 await,我们终于有了一种 在生成器函数内部真正暂停代码执行的能力。」25

消费端的不对称也要记:异步迭代器没有解构、不能展开传参—— 唯一的语法是 for await...of(和直接调 next())。 现实坐标:流式 API(ReadableStream)也是异步可迭代对象—— 「和网页版大模型对话,文字一段段往外冒」的打字机效果, 背后多半就是一个 for await...of 在逐块渲染。26

16.5 什么时候该写生成器

书里的收尾判断照录并展开——生成器的主业是「把产生值的过程包成 iterable, 让别人好消费」,三个甜区:

  1. 解构赋值一次取多个生成值(const [a, b] = generator);
  2. 递归遍历树的 for...of(DOM、文件系统、任何嵌套结构);
  3. 逐个处理昂贵任务的 for await...of(流式拉取、分批计算)。

「下次写着复杂循环时,问自己:写成生成器会不会更优雅」—— 但别忘了它的历史定位:它曾被寄望与 promise 一起终结回调地狱, async/await 出现后那个主战场没了;今天它是「小众但趁手」的工具, 不是必学的主线。27

17. 原文地图

主题 原书章 原文位置
promise 对象化三优点 Chapter 8: Functions for the future: promises and generators text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:43(搜「passed around freely」)
三态与一次性 同上 :68(搜「pending, fulfilled, or rejected」) · :73(搜「only change once」)
executor 与决议三途 同上 :85(搜「executor」) · :91(搜「throwing an error」)
thenable 跟随规则 同上 :105(搜「thenable」) · :132(搜「permanently pending」)
resolved vs settled 同上 :148(搜「RESOLVED VS. SETTLED」)
Promise.withResolvers 同上 :174(搜「Promise.withResolvers」)
then 返回新 promise 同上 :186(搜「returns another promise」)
fetch 链与三关错误 同上 :204(搜「Listing 8.3」) · :231(搜「three different places」)
finally 与静默拒绝坑 同上 :248(搜「finally method」) · :253(搜「silently ignored」)
Promise.all 语义 同上 :276(搜「Listing 8.4」) · :308(搜「all-or-nothing」)
any/race/allSettled 对照 同上 :320(搜「allSettled」) · :326(搜「Promise.any returns」)
race 掐表与 AbortController 同上 :343(搜「Listing 8.5」) · :375(搜「underused feature」)
p-limit 边栏 同上 :383(搜「LIMITING CONCURRENCY」)
微任务/宏任务三规则 同上 :400(搜「Of microtasks and macrotasks」) · :444(搜「strictly prioritized」)
输出顺序实验 同上 :409(搜「Listing 8.6」) · :426(搜「end of script」)
then 永不同步 同上 :432(搜「never call the then callback synchronously」)
规范分属两份 同上 :459(搜「TIMING ACCORDING TO THE SPECS」)
await 的语义与位置限制 同上 :548(搜「halts code execution」) · :583(搜「only allowed」)
await 行为三样例 同上 :563(搜「Phantom Blade」)
async 两条保证 同上 :606(搜「Automating promise creation」) · :628(搜「always return a promise」)
fetchJson 终态 同上 :657(搜「Listing 8.8」)
promisify 与 API 迁徙 同上 :682(搜「fs/promises」)
promise-function-async 同上 :648(搜「promise-function-async」)
生成器简史定位 同上 :691(搜「didn't work out that way」)
迭代器协议两条件 同上 :713(搜「Symbol.iterator」)
手动迭代数组 同上 :745(搜「Listing 8.9」)
解构=迭代的三个场合 同上 :764(搜「destructuring syntax」)
无限 iterable 示例 同上 :788(搜「Listing 8.10」)
生成器函数与生成器对象 同上 :809(搜「generator function」) · :826(搜「GENERATOR TERMINOLOGY」)
next/yield 协议三条 同上 :826(搜「iterator result returned from next」)
multiplier 双向通道 同上 :867(搜「Listing 8.12」)
yield* 委托与递归 同上 :888(搜「Listing 8.13」) · :919(搜「recursion」)
异步迭代协议 同上 :926(搜「Async iterables and async generators」)
fetchPosts 异步生成器 同上 :944(搜「Listing 8.14」)
for await 的消费限制 同上 :970(搜「aren't quite as versatile」)
ReadableStream 指认 同上 :974(搜「smooth flow of text」)
生成器适用判断 同上 :984(搜「main use case」)
全章总结 同上 :996(搜「Summary」)

  1. 出处:「Chapter 8: Functions for the future: promises and generators」第 68–146 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:137,搜「permanently pending」)。永远 pending 的 thenable 例子;跟随语义见第 121–130 段。 ↩↩

  2. 出处:「Chapter 8: Functions for the future: promises and generators」第 163–183 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:175,搜「Promise.withResolvers」)。executor 逃逸 hack 的历史与 withResolvers 的收编。 ↩↩

  3. 出处:「Chapter 8: Functions for the future: promises and generators」第 204–264 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:233,搜「deals with potential errors from three」)。一个 catch 接三处错误的清单;finally 的类比见第 248–251 段。 ↩↩

  4. 出处:「Chapter 8: Functions for the future: promises and generators」第 253–257 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:254,搜「silently ignored」)。静默失败的第一号抱怨与 no-floating-promises 规则。 ↩↩

  5. 出处:「Chapter 8: Functions for the future: promises and generators」第 266–317 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:311,搜「order of those values matches」)。all 的顺序保证原文。 ↩↩

  6. 出处:「Chapter 8: Functions for the future: promises and generators」第 339–380 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:378,搜「underused feature」)。race 掐表与 AbortController 的撤销语义。 ↩↩

  7. 出处:「Chapter 8: Functions for the future: promises and generators」第 383–395 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:383,搜「LIMITING CONCURRENCY」)。组合方法管不了有界并发及 p-limit 家族。 ↩↩

  8. 出处:「Chapter 8: Functions for the future: promises and generators」第 440–457 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:443,搜「strictly prioritized」)。微任务优先的循环纪律。 ↩↩

  9. 出处:「Chapter 8: Functions for the future: promises and generators」第 409–457 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:426,搜「reliably get this output」)。Listing 8.6 的输出;then 永不同步见第 432–435 段;4ms 最小延迟见第 437–438 段。 ↩↩

  10. 出处:「Chapter 8: Functions for the future: promises and generators」第 459–467 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:459,搜「TIMING ACCORDING TO THE SPECS」)。ECMAScript 与 HTML Living Standard 的管辖分工。 ↩↩

  11. 出处:「Chapter 8: Functions for the future: promises and generators」第 452–457 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:456,搜「queueMicrotask」)。官方微任务 API 与 React 批量更新的用例。 ↩↩

  12. 出处:「Chapter 8: Functions for the future: promises and generators」第 548–582 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:577,搜「complete stack trace」)。await 两步语义与「带完整栈的 throw」。 ↩↩

  13. 出处:「Chapter 8: Functions for the future: promises and generators」第 583–598 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:585,搜「only allowed」)。await 的两个合法位置与设计理由。 ↩↩

  14. 出处:「Chapter 8: Functions for the future: promises and generators」第 606–646 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:635,搜「always return a promise」)。async 两条规则与 Promise<string> 类型;拒绝原因 unknown 见第 638–640 段。 ↩↩

  15. 出处:「Chapter 8: Functions for the future: promises and generators」第 682–689 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:684,搜「fs/promises」)。API 从回调迁往 promise 的行业转向与 promisify。 ↩↩

  16. 出处:「Chapter 8: Functions for the future: promises and generators」第 647–651 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:648,搜「promise-function-async」)。规则与「错误该从 promise 来」的动机。 ↩↩

  17. 出处:「Chapter 8: Functions for the future: promises and generators」第 713–739 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:727,搜「Symbol.iterator」)。iterable 两条件与 Symbol 介绍。 ↩↩

  18. 出处:「Chapter 8: Functions for the future: promises and generators」第 745–762 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:745,搜「Listing 8.9」)。手动迭代 walk-through 与「nothing magical about arrays」。 ↩↩

  19. 出处:「Chapter 8: Functions for the future: promises and generators」第 764–803 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:770,搜「destructuring syntax」);与第 785 段(搜「be careful that the iterable is finite」)。三个隐藏用户与无限展开的警告。 ↩↩

  20. 出处:「Chapter 8: Functions for the future: promises and generators」第 788–807 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:788,搜「Listing 8.10」)。无限随机数 iterable 与「lazily generate values」。 ↩↩

  21. 出处:「Chapter 8: Functions for the future: promises and generators」第 809–843 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:833,搜「iterator result returned from next」)。生成器对象的三合一身份与 next 结果三形态。 ↩↩

  22. 出处:「Chapter 8: Functions for the future: promises and generators」第 826–832 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:828,搜「pauses the function's execution」)。yield 与 await 的暂停语义对照。 ↩↩

  23. 出处:「Chapter 8: Functions for the future: promises and generators」第 867–886 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:871,搜「Listing 8.12」)。multiplier 三断言与双向通道说明。 ↩↩

  24. 出处:「Chapter 8: Functions for the future: promises and generators」第 888–924 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:893,搜「Listing 8.13」)。yield* 委托示例;递归遍历 DOM 树见第 919–922 段。 ↩↩

  25. 出处:「Chapter 8: Functions for the future: promises and generators」第 926–968 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:968,搜「actually pause code execution」)。fetchPosts 与点睛句原文。 ↩↩

  26. 出处:「Chapter 8: Functions for the future: promises and generators」第 970–981 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:970,搜「aren't quite as versatile」)。异步迭代器的消费限制;ReadableStream 见第 974–977 段。 ↩↩

  27. 出处:「Chapter 8: Functions for the future: promises and generators」第 984–994 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:986,搜「main use case」)。生成器的适用判断与「oddity」的历史定位(第 704–707 段)。 ↩↩

  28. 出处:「Chapter 8: Functions for the future: promises and generators」第 593–597 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:594,搜「type": "module"」)。声明 ESM 的三种方式与 .mts 的选择。 ↩

  29. 出处:「Chapter 8: Functions for the future: promises and generators」第 606–646 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:628,搜「Returned values are converted to promises using the」)。async 的两条规则与 Promise<string> 类型。 ↩

  30. 出处:「Chapter 8: Functions for the future: promises and generators」第 682–688 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:684,搜「fs/promises」)。API 从回调迁往 promise 的行业转向与 promisify。 ↩

  31. 出处:「Chapter 8: Functions for the future: promises and generators」第 647–651 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:648,搜「promise-function-async」)。规则动机:错误该从 promise 来而非同步 throw。 ↩

  32. 出处:「Chapter 8: Functions for the future: promises and generators」第 691–707 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:704,搜「didn't work out that way」)。生成器期望落空史与「a bit of an oddity」的现状定位。 ↩

  33. 出处:「Chapter 8: Functions for the future: promises and generators」第 713–739 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:727,搜「Symbol.iterator」)。iterable 的两条件与 Symbol 介绍。 ↩

  34. 出处:「Chapter 8: Functions for the future: promises and generators」第 745–762 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:745,搜「Listing 8.9」)。手动迭代 walk-through 与「nothing magical about arrays」。 ↩

  35. 出处:「Chapter 8: Functions for the future: promises and generators」第 764–783 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:770,搜「This is commonly referred to as the array」)。解构/展开/剩余皆是迭代的三个场合。 ↩

  36. 出处:「Chapter 8: Functions for the future: promises and generators」第 788–803 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:788,搜「Listing 8.10」)。无限随机数 iterable;「be careful that the iterable is finite」的提醒见第 785 段。 ↩

  37. 出处:「Chapter 8: Functions for the future: promises and generators」第 809–823 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:810,搜「function keyword followed by *」)。生成器函数定义与「既是 iterable 又是 iterator」的生成器对象。 ↩

  38. 出处:「Chapter 8: Functions for the future: promises and generators」第 826–840 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:833,搜「iterator result returned from next」)。yield/return/throw 三种 next 结果形态。 ↩

  39. 出处:「Chapter 8: Functions for the future: promises and generators」第 828–835 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:828,搜「pauses the function's execution」)。yield 与 await 的暂停语义对比。 ↩

  40. 出处:「Chapter 8: Functions for the future: promises and generators」第 867–885 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:871,搜「Listing 8.12」)。multiplier 的 1、5、20 三断言与双向通道说明。 ↩

  41. 出处:「Chapter 8: Functions for the future: promises and generators」第 888–917 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:893,搜「Listing 8.13」)。yield* 委托与四行输出的对照。 ↩

  42. 出处:「Chapter 8: Functions for the future: promises and generators」第 919–924 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:919,搜「recursion」)。yield* 递归遍历 DOM 树的用例。 ↩

  43. 出处:「Chapter 8: Functions for the future: promises and generators」第 926–942 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:931,搜「ES2018 introduced async generators」)。异步迭代协议与键名 Symbol.asyncIterator。 ↩

  44. 出处:「Chapter 8: Functions for the future: promises and generators」第 944–966 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:968,搜「actually pause code execution」)。fetchPosts 完整例子与点睛句。 ↩

  45. 出处:「Chapter 8: Functions for the future: promises and generators」第 970–975 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:970,搜「aren't quite as versatile」)。异步迭代器的消费限制(无解构、无展开)。 ↩

  46. 出处:「Chapter 8: Functions for the future: promises and generators」第 974–981 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:976,搜「smooth flow of text」)。ReadableStream 与网页大模型打字机效果的联系。 ↩

  47. 出处:「Chapter 8: Functions for the future: promises and generators」第 984–993 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:986,搜「main use case」)。生成器的适用判断与原书的收尾建议。 ↩

  48. 出处:「Chapter 8: Functions for the future: promises and generators」第 121–130 段(text/11-ch08-chapter-8-functions-for-the-future-promises-and-.txt:122,搜「was fulfilled with value」);与 :426(搜「end of script」);与 :882(搜「multiplierGenerator」)。三组可复现断言。 ↩