未来方向 — Observable、Web Worker 与正在标准化的特性
未来方向 — Observable、Web Worker 与正在标准化的特性
这一章回答三个问题: Promise 还剩下哪两个盲区,Observable 怎么补; 重计算把界面卡死时,除了异步还能靠什么; 以及语言标准化正在给异步补哪些缺口(取消、顶层 await、错误链)。 读完你会知道什么时候该离开 Promise 去找 RxJS 或 Worker,以及"取消一个请求"今天标准答案长什么样。
1. 这一章讲什么
前面九章都在 Promise/async/await 的疆域里,这一章走到边界上,看三件事:
- Observable 与响应式编程(第 3 节)——补 Promise 的"一个值"与"不可取消"两个盲区;
- Web Worker(第 4 节)——补单线程结构管不了的"重计算";
- 正在标准化的特性(第 5 节)——top-level await、异步迭代器、AbortController、Error Cause。
2. 顶层全景:Promise 的疆界图
Promise 管不了的:
├─ 「值会陆续来很多个」(聊天消息、股价、用户输入)──→ Observable / 异步迭代器
├─ 「还没好,别等了」(取消)───────────────────→ AbortController / Observable 的取消
└─ 「算得太重,流水线不够用」───────────────────→ Web Worker(真正的第二线程)3. Observable:从一张票到一根水管
3.1 定义与主线例子
原书给定义:Observable 是"惰性、推送式的多值集合"——随时间陆续吐出多个值;Promise 只交付一个值,Observable 可以交付一串1。比方要换:Promise 是一张一次性票,Observable 是一根持续出水的水管。
import { Observable } from 'rxjs';
const observable = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => { subscriber.next(4); subscriber.complete(); }, 1000);
});
observable.subscribe({
next(x) { console.log('Got value ' + x); },
error(err) { console.error('Something wrong: ' + err); },
complete() { console.log('Done'); }
});输出时序(原书原样给出):1、2、3 同步到达,1 秒后 4 到达,然后 Done2。两个性质在这个例子里都能看见:
- 惰性:不
subscribe(订阅,即"我开始听了")就不出水——水管的阀门在订阅者手里; - 三态齐备:
next(每个值)、error(出错,管断)、complete(正常收口)。
3.2 组合运算:搜索框主线例子
单根水管没意思,有意思的在运算。原书的搜索框例子把三个操作串成一条响应式流水线3:
const searchInput$ = fromEvent(input, 'input').pipe(
map(event => event.target.value), // 事件 → 输入的文字
debounceTime(300) // 停手 300ms 才放行(防抖)
);
const stopClick$ = fromEvent(button, 'click');
searchInput$.pipe(takeUntil(stopClick$)) // 点了停止按钮,管道封口
.subscribe(value => console.log(`Searching for: ${value}`));主线走一遍(时刻是为演示编的):用户在 t=0、120、350 毫秒各敲一键,停手 300 毫秒后(t=650)发出一次搜索;t=900 按下停止键,之后再敲键不再触发。三个操作各自的角色:map 转换每个值,debounceTime 按时间窗筛选,takeUntil 按另一根水管封口。注意这段逻辑的声明感:"什么时候搜"被写成了数据管的形状,而不是一坨计时器状态——这正是响应式对命令式的主要卖点。
3.3 与 Promise 的对照表
| Promise | Observable | |
|---|---|---|
| 交付 | 一个值 | 一串值 |
| 启动 | 创建即执行 | 订阅才执行(惰性) |
| 取消 | 不能 | 退订(unsubscribe)即停 |
| 运算 | then/catch/finally | 上百个组合算子(pipe) |
常用库:RxJS(最流行)、Bacon.js、Most.js4。Observable 尚未进语言标准——这是它与本章其他主题的关键区别:用 RxJS 是引库,用 top-level await 是直接用语法。
3.4 Observable 与 Promise 的对照实验
"一根水管"和"一张票"的差别,用同一个需求各写一遍最清楚——需求:监听输入框,每次输入打一个值5:
// Promise 做不到这件事:它只能交付一次,第二次输入没有"第二张票"可言
// 回调能做,但组合三个操作(转换+防抖+停止)就要手写三个计时器状态
// Observable 版:三个操作各占一道工序
const searchInput$ = fromEvent(input, 'input').pipe(
map(event => event.target.value),
debounceTime(300)
);
searchInput$.pipe(takeUntil(stopClick$)).subscribe(...);这个对照里的每一句话都值得记住:Promise 的"只交付一次"不是缺点而是定义——让它管"持续的来",等于让信箱收瀑布;回调能收但每次叠加新操作就多一坨状态;Observable 把"持续"做成了第一公民,操作(防抖、取消、限流)于是成了现成工序而不是手写状态机。这就是 3.3 五个卖点里"统一抽象"与"时间操作"的实证。
3.5 取消:Promise 的盲区,Observable 的原生
把取消并排看,盲区看得最清6:
// Promise:没有"退票"按钮,只能赛跑(race 假装取消)或干等
// Observable:订阅句柄就是按钮
const subscription = searchInput$.subscribe(value => ...);
subscription.unsubscribe(); // 水关,后续工序全部停止订阅句柄(subscription)是 subscribe 的返回值,unsubscribe 一调,这根水管从出水口到工序链全部停止——不再回调、不再计时、不再发请求。与第 09 章 5.3 的 AbortController 对照:两者都是"把取消权交出去",区别是 Promise 的世界里取消要外挂一对 controller/signal,Observable 的世界里取消内生在每个订阅里。第 12 章 3.3 把"可取消"列为 RxJS 第一卖点,根据就在这里。
4. Web Worker:异步管"等",线程管"算"
4.1 分工
异步解决的是"等"(I/O 时让出流水线),但如果任务是算(斐波那契、图像滤镜、大数据处理),再漂亮的异步也救不了——计算必须真在另一条硬件流水线上跑。Web Worker 就是浏览器开的第二条(第 N 条)JavaScript 线程7。
主线程与 Worker 之间不共享内存里的随便什么,全靠发消息(postMessage)通信:
// 主线程
const worker = new Worker('worker.js');
worker.onmessage = event => console.log('Received from worker:', event.data);
worker.postMessage('Hello, Worker!');
// worker.js(跑在另一条线程)
self.onmessage = event => {
console.log('Received in worker:', event.data);
self.postMessage('Hello, Main thread!');
};4.2 主线例子:斐波那契搬上 Worker
原书把计算斐波那契数列(递归:自己调用自己、越调数字越小直到停手;数字稍大就慢得离谱)搬到 Worker:主线程一口气发 45 个计算请求(for 循环里 postMessage),Worker 逐个算完回发,界面全程可点8。
4.3 三类 Worker 与四条限制
三类9:
| 类型 | 谁在用 | 典型用途 |
|---|---|---|
| 专用 Worker(Dedicated) | 单个脚本 | 后台计算(最常用) |
| 共享 Worker(SharedWorker) | 同域多个窗口/iframe | 跨页共享状态 |
| Service Worker | 站在应用与网络之间 | 缓存与离线(第 07 章 4.3 已现身) |
四条限制10:不能碰 DOM(界面更新必须回主线程);能用的浏览器接口有限;主线程与 Worker 之间搬大数据有开销(消息是拷贝,不是共享);每个 Worker 有自己的全局作用域。判断"该不该用 Worker"就一句话:任务是等还是算——等,留在异步;算,搬去 Worker(第 00 章 4 节那条选型线的闭合)。
4.4 Worker 的消息流:45 个请求的时间线
4.2 的斐波那契例子,把主线程与 Worker 之间的消息流画成时间线(数字为演示编)11:
主线程: Worker(另一条流水线):
t=0 连发 45 条 postMessage t=0 收到第 1 条,开始算 fib(0)——瞬间完
({type:'calculate', n:0..44}) t=0.1 回发结果,收到第 2 条,fib(1)……
t=大 越算越慢:fib(40+) 要数秒
t=0.5 界面点击、滚动完全正常 ←─────── 全程不影响主线程
陆续收到 worker.onmessage,逐条更新界面两个要点。其一,消息是异步的:postMessage 不等 Worker 处理,主线程发完 45 条只花一瞬;结果通过 onmessage 陆续回来,顺序与发出一致(消息队列保证)。其二,大数据拷贝的代价:消息传的是拷贝——传一个 100MB 的数组给 Worker,真复制 100MB(浏览器后来加了可转移对象的优化,把数据"搬"过去而不是"拷"过去,书未展开)。这个例子也因此有个隐含边界:任务要"值得"——计算量得大过拷贝开销,否则 Worker 反而更慢。
4.5 三类 Worker 的选型
4.3 的三类,用两个问题选12:
谁要用它? 一个脚本 → 专用 Worker;同域多个窗口/iframe 要共享 → 共享 Worker。 站在哪? 应用与网络之间(代理请求、管缓存)→ Service Worker;只是后台算 → 前两者。
Service Worker 在第 07 章 4.5 已以"离线页"身份出场,这里的定位是它更一般的角色:它能拦截应用的所有网络请求,所以缓存、离线、推送提醒都归它管。注意它的生命周期与页面无关(页面关了它还在),调试也单独一块(Application 面板)——书里没展开,但它和"异步"的关系最间接,把它当"会异步的网络代理"理解即可。
5. 正在标准化的特性
5.1 top-level await(ES2022)
以前 await 只能写在 async 函数里,模块顶层想要异步结果,得套一个"立即执行的 async 函数"的壳。top-level await 把这个壳拆了——模块顶层直接写 await,而且整个模块的导入方会等它完成13:
// data-fetcher.js
const response = await fetch('https://api.example.com/data');
export const data = await response.json();
// main.js —— import 拿到 data 时,它早已就位
import { data } from './data-fetcher.js';5.2 异步迭代器与生成器
for await...of 循环:数据源是一个不断吐值的异步源(异步生成器 async function*),循环体逐个 await 地取14:
async function* fetchPages(urls) {
for (const url of urls) {
const response = await fetch(url);
yield await response.text(); // 每轮吐一页
}
}
for await (const page of fetchPages(urls)) {
console.log(page); // 取到一页,处理一页
}与 Promise.all 的差别:all 是"全到齐再开工",异步迭代是"到一个开工一个"——后者省内存、能处理无限流。
5.3 AbortController:标准的取消
取消请求曾经是各家自己造轮子;现在的标准答案是 AbortController(中止控制器):它造一个 signal(信号)递给 fetch,谁想取消就调 controller.abort()15:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5 秒没完就取消
try {
const response = await fetch(url, { signal: controller.signal });
...
} catch (err) {
if (err.name === 'AbortError') console.log('Fetch aborted'); // 取消走这个分支
}Promise 不能取消的盲区,由这对搭档补上。第 04 章 3.2 的"请求 vs 超时赛跑"是它的雏形——race 是假装超时,abort 是真取消(连接真的被掐断了)。
5.4 Error Cause 与异步上下文
异步上下文,就是"这次异步是为什么发起的"这一信息。new Error('消息', { cause: 原始错误 }) 把"包装错误"和"保留原始错误"一次做成——err.cause 顺着链摸到根因,异步多层转手上尤其有用16。异步上下文追踪(对标 Node.js 的 async_hooks)还在提案阶段,目标是让日志、错误栈自动带上"这次异步是为什么发起的"17。
5.6 top-level await 之前的世界:IIFE workaround 的退场史
5.1 说 top-level await(顶层等待)省了一个壳,这个壳的来历值得知道18:
// ES2022 之前:想要"模块加载时就把异步数据取好",必须包一层立即执行的 async 函数
(async () => {
const data = await fetchData();
initApp(data);
})();
// 这带来两个别扭:initApp 被关进壳里,模块外无法同步使用 data;
// 错误要专门挂 .catch,否则壳里的拒绝无人接。
// ES2022 之后:顶层直接写
const data = await fetchData(); // 导入方会等这一行完成
initApp(data);退场的不仅是括号。导入方的语义变化更要紧:旧 workaround 里,import 这个模块拿到的是"还在后台跑的壳",谁都不知道数据何时就绪;有了 top-level await,import 会等模块顶层的 await 全部落定——导入方拿到的就是就绪的数据。这把"模块的异步初始化"从约定俗成的技巧变成了语言保证。代价也有:被顶层 await 卡住的模块会拖慢整个导入链,所以适合"启动必需的数据"(配置、当前用户),不适合"晚点再用的大数据集"。
5.7 AbortController 的三角色
5.3 的例子有三个名词,关系固定,认全了取消就通了19:
AbortController(控制器)── .signal(信号)──→ fetch(url, { signal })
│ │
└────── .abort() 一按 ───────────────────┘ → fetch 立即中断,抛 AbortError- controller:开关的持有者,通常在发起请求的那一层造;
- signal:开关的状态线,作为参数递给要取消的操作(fetch 认它,越来越多的 API 也认它);
- abort():按开关的动作——按下去,持有这个 signal 的所有操作一起收到中止信号。
一对多也成立:多个请求共享一个 signal,abort 一按全部取消(场景:用户点了"刷新",旧的一窝请求集体作废)。第 13 章场景四的 { promise, cancel } 对象,就是把 controller 与它的 abort 打包成一个"取消按钮"交给调用方——三角色之外再加一个"按钮的接收者"。
6. 作者的判断与证据
本章的判断性最强,因为它写的是"未来"。值得带走的判断有两处:一是 Observable 被放在未来章而非正文,暗示作者认为 Promise/async/await 仍是主流,响应式是特定场景(数据持续到来)的利器1;二是 Worker 的"等 vs 算"分工被反复强调——作者显然认为"异步解决一切"是常见误解,重计算必须上线程7。
证据形态较弱:本章代码依赖 RxJS 库与较新浏览器,可复现性依赖环境,与前几章"贴进控制台就能跑"不同;top-level await 的语义("导入方会等待")书里也只演示了行为,没有规范引用。
7. 边界与局限
- RxJS 只到"认识算子"的程度,marble 图、热冷 Observable、Subject 家族这些 RxJS 的主干内容未碰;
- Worker 的消息通信只讲了 postMessage,没讲 Transferable(零拷贝转移大数据)——而 4.3 说的"搬数据有开销"正靠它缓解,书没接上;
- SharedWorker 的兼容性与 Service Worker 的生命周期(安装/激活/更新)未展开;
- 异步上下文追踪只有一段概念描述,没有代码。
8. 可带走的
- Promise 的两个盲区:只交付一个值、不能取消;前者 Observable/异步迭代器补,后者 AbortController 补;
- Observable 三态:next/error/complete;惰性——订阅才执行,退订即取消;
- 响应式搜索框三件套:map(转换)→ debounceTime(防抖)→ takeUntil(封口);
- 等用异步,算用 Worker;Worker 不见 DOM,主仆之间靠 postMessage 发消息;
- top-level await(ES2022):模块顶层直接 await,导入方自动等待;
- for await...of:到一个处理一个,与 Promise.all 的"齐收"互补;
- AbortController + signal 是取消的标准答案;取消的错误是 AbortError,要单独认。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 本章三大方向 | Chapter 9: The Future of Asynchronous JavaScript | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:7(搜「Observables」) |
| Observable 定义 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:15(搜「lazy」) |
| Observable 例子与输出 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:21(搜「new Observable」) · text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:41(搜「Just before subscribe」) |
| 响应式搜索框 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:65(搜「fromEvent」) |
| RxJS 等库 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:94(搜「RxJS」) |
| Worker 定义与消息通信 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:108(搜「background threads」) · text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:119(搜「postMessage」) |
| 三类 Worker | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:133(搜「Dedicated Workers」) |
| 斐波那契 Worker | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:157(搜「fibonacci」) |
| Worker 限制 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:183(搜「No DOM Access」) |
| top-level await | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:199(搜「ECMAScript 2022」) |
| 异步迭代器与生成器 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:218(搜「for-await-of」) |
| AbortController | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:245(搜「AbortController」) |
| Error Cause | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:270(搜「Error Cause」) |
| 异步上下文追踪 | 同上 | text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:298(搜「async_hooks」) |
Footnotes
-
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 15 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:15,搜「lazy」)。原文:"Observables are lazy push collections of multiple values… as opposed to Promises which resolve to a single value." ↩ ↩2 -
出处:同章第 31-49 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:41,搜「Just before subscribe」)。输出序列照原书:三个值同步到达,1 秒后第 4 个值,然后 Done。 ↩ -
出处:同章第 65-88 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:73,搜「debounceTime」;:79,搜「takeUntil」)。map→debounceTime→takeUntil 的流水线照原书。 ↩ -
出处:同章第 92-100 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:94,搜「RxJS」)。RxJS、Bacon.js、Most.js 三个库照原书。 ↩ -
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 63-88 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:65,搜「fromEvent」)。搜索框例子的三道工序照原书。 ↩ -
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 57-61 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:59,搜「Cancellation」)。"Subscriptions to Observables can be cancelled" 照原书。 ↩ -
出处:同章第 104-108 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:104,搜「single-threaded」;:108,搜「background threads」)。 ↩ ↩2 -
出处:同章第 151-177 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:160,搜「45」)。主线程循环 postMessage 45 个计算请求照原书。 ↩ -
出处:同章第 129-137 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:133,搜「Dedicated」;:135,搜「Shared」;:137,搜「Service」)。三类 Worker 照原书。 ↩ -
出处:同章第 179-191 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:183,搜「No DOM Access」;:187,搜「Communication Overhead」)。四条限制照原书。 ↩ -
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 151-177 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:160,搜「45」)。45 个请求的 postMessage 循环与 fibonacci 实现照原书。 ↩ -
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 129-149 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:133,搜「Dedicated」;:135,搜「Shared」;:137,搜「Service」)。三类 Worker 照原书。 ↩ -
出处:同章第 197-214 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:199,搜「ECMAScript 2022」)。top-level await 已入 ES2022,模块顶层可用。 ↩ -
出处:同章第 216-241 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:222,搜「async function*」;:236,搜「for await」)。异步生成器与 for await...of 照原书。 ↩ -
出处:同章第 243-266 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:249,搜「AbortController」;:259,搜「AbortError」)。signal 传递、abort 调用、AbortError 识别照原书。 ↩ -
出处:同章第 268-294 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:278,搜「cause」)。new Error('API request failed', { cause: response.statusText })与 err.cause 读取照原书。 ↩ -
出处:同章第 296-306 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:298,搜「async_hooks」)。"proposals and discussions around better ways to track asynchronous context"。 ↩ -
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 197-214 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:199,搜「ECMAScript 2022」)。top-level await 的用法与"eliminating the need for complex workarounds"照原书;导入方等待的语义为补充(来自规范,与原书示例一致)。 ↩ -
出处:「Chapter 9: The Future of Asynchronous JavaScript」第 243-266 段(
text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:249,搜「AbortController」;:259,搜「AbortError」)。controller/signal/abort 的用法与 AbortError 分支照原书。 ↩