跳转至

未来方向 — Observable、Web Worker 与正在标准化的特性

这一章回答三个问题: Promise 还剩下哪两个盲区,Observable 怎么补; 重计算把界面卡死时,除了异步还能靠什么; 以及语言标准化正在给异步补哪些缺口(取消、顶层 await、错误链)。 读完你会知道什么时候该离开 Promise 去找 RxJS 或 Worker,以及"取消一个请求"今天标准答案长什么样。

1. 这一章讲什么

前面九章都在 Promise/async/await 的疆域里,这一章走到边界上,看三件事:

  1. Observable 与响应式编程(第 3 节)——补 Promise 的"一个值"与"不可取消"两个盲区;
  2. Web Worker(第 4 节)——补单线程结构管不了的"重计算";
  3. 正在标准化的特性(第 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 的对照实验

"一根水管"和"一张票"的差别,用同一个需求各写一遍最清楚——需求:监听输入框,每次输入打一个值14:

// 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 的原生

把取消并排看,盲区看得最清15:

// 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 线程5。

主线程与 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 逐个算完回发,界面全程可点6。

4.3 三类 Worker 与四条限制

三类7:

类型 谁在用 典型用途
专用 Worker(Dedicated) 单个脚本 后台计算(最常用)
共享 Worker(SharedWorker) 同域多个窗口/iframe 跨页共享状态
Service Worker 站在应用与网络之间 缓存与离线(第 07 章 4.3 已现身)

四条限制8:不能碰 DOM(界面更新必须回主线程);能用的浏览器接口有限;主线程与 Worker 之间搬大数据有开销(消息是拷贝,不是共享);每个 Worker 有自己的全局作用域。判断"该不该用 Worker"就一句话:任务是等还是算——等,留在异步;算,搬去 Worker(第 00 章 4 节那条选型线的闭合)。

4.4 Worker 的消息流:45 个请求的时间线

4.2 的斐波那契例子,把主线程与 Worker 之间的消息流画成时间线(数字为演示编)16:

  主线程:                              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 的三类,用两个问题选17:

谁要用它? 一个脚本 → 专用 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,而且整个模块的导入方会等它完成9:

// 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 地取10:

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()11:

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 顺着链摸到根因,异步多层转手上尤其有用12。异步上下文追踪(对标 Node.js 的 async_hooks)还在提案阶段,目标是让日志、错误栈自动带上"这次异步是为什么发起的"13。

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 算"分工被反复强调——作者显然认为"异步解决一切"是常见误解,重计算必须上线程5。

证据形态较弱:本章代码依赖 RxJS 库与较新浏览器,可复现性依赖环境,与前几章"贴进控制台就能跑"不同;top-level await 的语义("导入方会等待")书里也只演示了行为,没有规范引用。

7. 边界与局限

  • RxJS 只到"认识算子"的程度,marble 图、热冷 Observable、Subject 家族这些 RxJS 的主干内容未碰;
  • Worker 的消息通信只讲了 postMessage,没讲 Transferable(零拷贝转移大数据)——而 4.3 说的"搬数据有开销"正靠它缓解,书没接上;
  • SharedWorker 的兼容性与 Service Worker 的生命周期(安装/激活/更新)未展开;
  • 异步上下文追踪只有一段概念描述,没有代码。

8. 可带走的

  1. Promise 的两个盲区:只交付一个值、不能取消;前者 Observable/异步迭代器补,后者 AbortController 补;
  2. Observable 三态:next/error/complete;惰性——订阅才执行,退订即取消;
  3. 响应式搜索框三件套:map(转换)→ debounceTime(防抖)→ takeUntil(封口);
  4. 等用异步,算用 Worker;Worker 不见 DOM,主仆之间靠 postMessage 发消息;
  5. top-level await(ES2022):模块顶层直接 await,导入方自动等待;
  6. for await...of:到一个处理一个,与 Promise.all 的"齐收"互补;
  7. 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」)


  1. 出处:「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。 ↩

  3. 出处:同章第 65-88 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:73,搜「debounceTime」;:79,搜「takeUntil」)。map→debounceTime→takeUntil 的流水线照原书。 ↩

  4. 出处:同章第 92-100 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:94,搜「RxJS」)。RxJS、Bacon.js、Most.js 三个库照原书。 ↩

  5. 出处:同章第 104-108 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:104,搜「single-threaded」;:108,搜「background threads」)。 ↩↩

  6. 出处:同章第 151-177 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:160,搜「45」)。主线程循环 postMessage 45 个计算请求照原书。 ↩

  7. 出处:同章第 129-137 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:133,搜「Dedicated」;:135,搜「Shared」;:137,搜「Service」)。三类 Worker 照原书。 ↩

  8. 出处:同章第 179-191 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:183,搜「No DOM Access」;:187,搜「Communication Overhead」)。四条限制照原书。 ↩

  9. 出处:同章第 197-214 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:199,搜「ECMAScript 2022」)。top-level await 已入 ES2022,模块顶层可用。 ↩

  10. 出处:同章第 216-241 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:222,搜「async function*」;:236,搜「for await」)。异步生成器与 for await...of 照原书。 ↩

  11. 出处:同章第 243-266 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:249,搜「AbortController」;:259,搜「AbortError」)。signal 传递、abort 调用、AbortError 识别照原书。 ↩

  12. 出处:同章第 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 读取照原书。 ↩

  13. 出处:同章第 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"。 ↩

  14. 出处:「Chapter 9: The Future of Asynchronous JavaScript」第 63-88 段(text/12-ch09-chapter-9-the-future-of-asynchronous-javascript.txt:65,搜「fromEvent」)。搜索框例子的三道工序照原书。 ↩

  15. 出处:「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" 照原书。 ↩

  16. 出处:「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 实现照原书。 ↩

  17. 出处:「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 照原书。 ↩

  18. 出处:「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"照原书;导入方等待的语义为补充(来自规范,与原书示例一致)。 ↩

  19. 出处:「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 分支照原书。 ↩