再进一步 — RxJS、框架里的异步与性能优化¶
这一章回答三个问题: 学完 Promise/async/await 之后,接下来该往哪三个方向深入; 同样的取数逻辑,在 React 和 Vue 里分别封装成什么形状; 以及性能优化该从哪里下手。 读完你会认出三个框架里的异步代码其实在干同一件事,并且有一份性能检查清单。
1. 这一章讲什么¶
这是全书的"出站口",三条进阶路:
- RxJS:Observable 的实战化(第 3 节);
- 框架里的异步(第 4 节):React 与 Vue 如何封装取数逻辑;
- 性能优化清单(第 5 节):时序、内存、网络、计算。
2. 顶层全景:同一个想法,三种容器¶
本章藏着一个统一模式,先点破再看细节:
纯 JavaScript: 把"取数+状态"封装成一个函数(fetchUserDataAndPosts)
React: 把同一套逻辑封装成一个自定义 hook(useFetch)
Vue 3: 把同一套逻辑封装成一个组合式函数(useFetch)
RxJS: 把同一套逻辑封装成一根可运算的水管( Observable 管道)
容器不同,内容一样:发起请求、管理"加载中/出错"状态、交付结果。 学会在框架代码里认出这个骨架,第 4 节就只是"查容器说明书"。
3. RxJS:Observable 的实战三件套¶
3.1 算子:水管的加工工序¶
第 09 章认识了 Observable,本章上手运算。of(1,2,3,4,5) 造一根吐五个数的水管,pipe 接两道工序:filter(只留下偶数)→ map(各自平方),subscribe 收到 4 和 161:
const numbers$ = of(1, 2, 3, 4, 5);
numbers$.pipe(
filter(n => n % 2 === 0),
map(n => n * n)
).subscribe(x => console.log(x)); // 4, 16
工序的名字就是行为:filter 筛选,map 逐个转换。RxJS 有上百个这样的算子,但入门只需要记住pipe 里从左到右依次加工这一个结构。
3.2 多请求组合:mergeMap 与 forkJoin¶
取用户 → 取该用户的帖子 → 合成,用 RxJS 写是2:
getUser(1).pipe(
mergeMap(user => forkJoin({ // 拿到用户后,分叉去取帖子
user: of(user),
posts: getUserPosts(user.id)
})),
map(({ user, posts }) => ({ ...user, posts }))
).subscribe(result => console.log(result));
两个新名字:mergeMap(把每个值换成一根新水管并合并输出)与 forkJoin(多根水管一起等,全到齐合成一个对象——语义上就是 Observable 世界的 Promise.all)。对照第 03-04 章:这套管道与 Promise 链+all 干的是同一件事,写法从"操作票"变成"描述水管"。
3.3 为什么值得学:五个卖点¶
原书列了五条3:统一抽象(事件、请求、WebSocket 都成水管)、可组合(算子链)、健壮的错误处理、可取消(退订即停——Promise 至今没有的东西)、内建时间操作(防抖、节流、定时——第 06 章手写 debounce 在这里是一个算子)。
3.4 RxJS 的适用边界¶
3.3 列了五个卖点,什么时候不该上 RxJS,同样值得说清12:
不该的场景: 一次性的取数(async/await 三行就完);逻辑简单的单次事件(点击一次做一件事);团队没人会响应式(读 pipe 像读天书的团队,维护成本远超收益)。
该的场景: 同一个事件源上叠着多个时间操作(输入 → 防抖 → 取消旧请求 → 重试,这类"事件 + 时间 + 取消"的组合正是 Promise 的盲区);数据来自多个推送源(WebSocket + 用户输入 + 定时器)需要合流;需要精细的开销控制(退订即停,内存与请求都被管住)。
一句话边界:Promise 管"一次的等",RxJS 管"持续的来"。 判断不清时,从 Promise 起步——把单次的等写清楚之后,如果代码里开始出现成堆的计时器与标志位,那就是 RxJS 在敲门了(第 09 章 3.2 搜索框例子的"成堆计时器"形态,被 pipe 三行收编,就是敲门声)。
3.5 本章与第 09 章的分工¶
两章都讲 Observable,分工不同:第 09 章管"是什么"(定义、三态、与 Promise 的对照),本章管"怎么用"(算子、组合、五个卖点)。读法上,第 09 章 3.1 的"出水时间线"例子是地基,看不懂它就回头;本章的 forkJoin/mergeMap 是水管的接法,可以按需查。学习顺序上,第 09 章是不可少的,本章是可以"知道存在、用时再查"的——这也是为什么它叫"What's Next"。
4. 框架里的异步:React 与 Vue¶
4.1 React:useEffect + 自定义 hook¶
React 本身不管异步;约定是副作用(包括取数)放 useEffect,数据存组件状态4:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
setUser(await response.json());
} catch (error) { console.error('Failed to fetch user:', error); }
}
fetchUser();
}, [userId]); // userId 变了重新取
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
一个细节:async 函数定义在 effect 里面再调用,而不是把 effect 回调本身标 async——effect 的返回值另有用途(清理函数),所以不能是 Promise4。
把这套逻辑提成自定义 hook useFetch(url):内部三态(data/loading/error),返回给组件直接用5。第 10 章天气应用的"取数与显示分离",在 React 里就长成了这个 hook——对照 2 节那张图。
4.2 Vue:watcher、异步组件、组合式 API¶
- watcher 触发取数:数据变(userId 变)→ watcher 的 handler 自动跑取数,
immediate: true让组件创建时也跑一次6; - 异步组件:组件本身按 import 懒加载,加载中显示占位组件、失败显示错误组件、可设超时——把"代码也要异步取"纳入同一套心智7;
- 组合式 API( Vue 3)的 useFetch:与 React 自定义 hook 同构——
ref(null)装数据、onMounted(fetchData)挂钩子生命周期,返回{ data, error, refresh },其中refresh把重取能力也交出去8。
4.3 三种容器的对照¶
| 纯 JS | React | Vue 3 | |
|---|---|---|---|
| 封装单元 | 函数 | 自定义 hook | 组合式函数 |
| 状态容器 | 变量 | useState | ref |
| 挂载时机 | 调用处 | useEffect | onMounted |
| 三态(值/载/错) | 自己约定 | hook 内返回 | 同左 |
4.4 useEffect 依赖数组的异步含义¶
4.1 的例子有一个没展开的细节——useEffect(..., [userId]) 的依赖数组13:
useEffect(() => {
async function fetchUser() { ... }
fetchUser();
}, [userId]); // userId 变了,这个 effect 重跑
依赖数组是 React 的"什么时候重来"清单:userId 不变,效果不重跑(省请求);变了,旧的效果丢弃、新的 fetch 发起。放在异步语境里,这相当于把第 11 章竞态的"取消旧请求"需求交给了框架——虽然这个最小例子还没真的取消(旧请求仍在途,只是它的 setUser 落在已卸载的旧组件上会被忽略)。要真取消,得在 effect 里返回清理函数(那里是 AbortController 的家,第 09 章 5.3 的应用位)。
4.6 Vue 的 watcher:响应式触发的异步¶
Vue 的 watcher 例子里藏着"响应式"与"异步"的接合点18:
export default {
data() { return { userId: 1, user: null } },
watch: {
userId: {
handler: async function(newId) {
const response = await fetch(`/api/users/${newId}`);
this.user = await response.json();
},
immediate: true // 组件创建时也先跑一遍
}
}
}
与 React 的 useEffect 对照:userId 变化触发 watcher,等价于依赖数组命中重跑;immediate: true 等价于挂载即执行。响应式框架的共同承诺是"数据变 → 相关动作自动重跑"——异步取数只是这些动作里最常见的一种。写法差异不小,心智模型一致:声明"依赖什么"与"做什么",框架负责在正确的时刻叫醒你。
4.7 异步组件:把"加载代码"也异步化¶
Vue 异步组件(4.2)的四个字段值得逐个说19:
const AsyncComponent = () => ({
component: import('./AsyncComponent.vue'), // 真正的异步:组件代码也是网络取的
loading: LoadingComponent,
error: ErrorComponent,
delay: 200, // 加载超过 200ms 才显示 loading(闪一下的加载比没有更烦)
timeout: 3000 // 3000ms 没到 → error
});
component: import(...) 是这个写法的灵魂:组件的代码本身也成了异步取数的对象——首屏只加载首屏组件,路由深处的组件等用户点到再取。delay 与 timeout 两个参数,是把第 10 章 3.4 状态清单(加载中/失败)与"多久算慢"的产品判断一并内置了。React 里对应的概念是 lazy() + Suspense,形状不同,思想相同:代码也是要取的东西,加载它同样要管三态。
4.5 三态模式:loading/error/data¶
三个容器都收敛到同一个"三态"形状,值得作为模式单独记住14:
// React 的 useFetch 返回:{ data, loading, error }
// Vue 的 useFetch 返回:{ data, error, refresh }
// 手写版:let 变量三个,各管一态
每个取数单元都该能回答三个问题:数据在哪(data)?还在等吗(loading)?是不是挂了(error)?界面按三态渲染(加载中显示转圈、错误显示文案、有数据显示内容),这个组件就永远不会出现"第四种状态"——那种谁都不处理的灰色地带。第 10 章天气应用的状态清单(3.4)就是这个模式的纯 JavaScript 版;到了框架里,三态被收进变量而不是 DOM,可测试性上了一个台阶。
5. 性能优化清单¶
原书给了五个下手点9:
- 微任务 vs 宏任务的时序:会推 3.3 的五行输出题(
Promise先于setTimeout),才谈得上优化时序; - 内存泄漏三源:忘清的定时器/间隔、永远不 settle 的 Promise(挂着就回收不了)、闭包长抓大对象——长期运行的应用(单页应用)要定期自查;
- 网络三招:防抖(停手 300ms 才发请求)、缓存(别重复取)、请求合并(能一次送的不分十次);
- 重计算搬 Worker(第 09 章 4 节的选型线再强调);
- Performance/Memory 面板:录制-分析的运行时体检工具。
5.1 五个优化方向的优先级¶
清单五条不是平级,按"做错代价"排个序15:
- 内存泄漏三源(忘清的定时器、悬挂的 Promise、长抓大对象的闭包)——代价是应用越跑越慢直至崩溃,且最难事后排查,先查;
- 网络三招(防抖/缓存/合并)——代价是用户体验与服务器账单,收益立竿见影;
- 重计算搬 Worker——代价是界面卡死,但只在有重计算时存在;
- 微任务宏任务时序——代价是偶发的诡异 bug,先会推输出(第 01 章)再谈优化;
- 面板录制——不是优化项,是找到"该优化哪条"的仪器。
前两条是"每次写代码都要想着"的,后三条是"出问题才上场"的。清单的正确用法是前三条日常、后两条备查,而不是五条一起背。
5.2 三条进阶路的学习顺序建议¶
第 1 节列的三条路,怎么排学习顺序16:
先框架(第 4 节),后 RxJS(第 3 节)。 理由是需求驱动:换了 React/Vue 的工作,框架的 useEffect/useFetch 立刻要用,而"事件流 + 时间操作"的需求未必天天出现;等真的被计时器淹没时,RxJS 的入门成本(半天)才花得值。反过来先学 RxJS 的风险是:没有真实的多流场景练手,算子背了就忘——第 09 章 3.2 的搜索框例子之所以好,正因为它是一个人人都有的真实场景。
性能优化(第 5 节)不排期,融入日常。 五条不是一门课,是五个检查点:写定时器时想着清、写请求时想着防抖与缓存、写重计算时想着 Worker。等"学完再应用"的人,永远学不完——因为它们散在第 01-11 章的每一章里。
共同的地基是第 01-08 章。 无论走进哪条路,票、链、队列、错误传播这些概念一个字都不会浪费:React 的 useFetch 里是 Promise,RxJS 的水管里是事件,Worker 的消息旁还是事件循环——容器换了,内容没换。
5.3 异步上下文追踪:值得跟踪的提案¶
5 节的 Error Cause 与异步上下文追踪,前者已落地(ES2022),后者还在路上,值得留一个关注的理由17:异步上下文的痛,排过跨多次 await 的线上 bug 的人都懂——日志里的两次请求,说不清是不是同一次用户操作发起的。Node.js 的 async_hooks 已在服务端提供了底层能力(书点到为止),浏览器与语言层的标准化(AsyncContext 一类提案)目标是把"这次异步是为什么发起的"变成跨环境的可用信息。它落地后的直接收益:错误栈与日志自动带上操作 ID,第 07 章 5.7 那个 context 字段(手动补的业务现场)有希望在语言层自动获得。现在不用追,知道"这个问题官方在解"就够。
6. 作者的判断与证据¶
本章的判断是路线图式的:作者认为异步学习的下一站是"响应式抽象 + 框架容器 + 性能意识",而非更深的 Promise 内部10。证据依然是代码对照——React 与 Vue 的 useFetch 并排,读者能亲眼看到 4.3 那张对照表不是凑的。
收尾处作者给了一句祝福式的总结:"愿你所有的 Promise 都成功兑现"(may your Promises always resolve successfully)——全书到此收在一个轻巧的梗上11。
7. 边界与局限¶
- RxJS 部分仍是"认识算子"级,Subject、热冷 Observable、错误重试算子(retryWhen)等主干未涉;
- 框架部分停在 Options API/Composition API 的基础对照,没讲数据获取库(TanStack Query/SWR)——真实项目里它们接管了 useFetch 的活;
- 性能清单每条都只给方向不给判据(防抖 300ms 怎么定?缓存多久?),深入要靠 profiling 实证;
- 服务端渲染(SSR)中的异步取数(Next.js 的 getServerSideProps、Nuxt 的 asyncData)完全没提。
8. 可带走的¶
- RxJS 三件套:pipe 工序链(filter/map)、mergeMap(分叉合并)、forkJoin(Observable 界的 Promise.all);
- RxJS 相对 Promise 的真增量是取消与时间操作;事件流、WebSocket 场景优先考虑它;
- React/Vue 里的取数单元是同构的:值/载/错三态 + 生命周期挂钩 + 可复用封装;
- useEffect 的回调不能是 async——async 函数要定义在里面再调用;
- 性能清单五下手点:时序、泄漏三源、网络三招、Worker、面板录制;
- 框架只是容器:第 03-06 章的票、链、并行、重试,装进哪个容器都还是它们。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 章旨与三条路 | Chapter 12: What's Next? | text/15-ch12-chapter-12-what-s-next.txt:7(搜「advanced topics」) |
| Observable 与算子 | 同上 | text/15-ch12-chapter-12-what-s-next.txt:21(搜「new Observable」) · text/15-ch12-chapter-12-what-s-next.txt:43(搜「Operators」) · text/15-ch12-chapter-12-what-s-next.txt:55(搜「output will be」) |
| forkJoin/mergeMap | 同上 | text/15-ch12-chapter-12-what-s-next.txt:61(搜「forkJoin」) |
| 为什么学 RxJS | 同上 | text/15-ch12-chapter-12-what-s-next.txt:96(搜「Cancellation」) |
| React useEffect | 同上 | text/15-ch12-chapter-12-what-s-next.txt:114(搜「useEffect」) · text/15-ch12-chapter-12-what-s-next.txt:138(搜「not allowed」) |
| React 自定义 hook | 同上 | text/15-ch12-chapter-12-what-s-next.txt:146(搜「useFetch」) |
| Vue watcher / 异步组件 | 同上 | text/15-ch12-chapter-12-what-s-next.txt:192(搜「Watchers」) · text/15-ch12-chapter-12-what-s-next.txt:222(搜「Async Components」) |
| Vue 组合式 useFetch | 同上 | text/15-ch12-chapter-12-what-s-next.txt:236(搜「Composition API」) |
| 微任务宏任务/泄漏/网络/Worker | 同上 | text/15-ch12-chapter-12-what-s-next.txt:273(搜「Microtasks」) · text/15-ch12-chapter-12-what-s-next.txt:295(搜「Memory Leaks」) · text/15-ch12-chapter-12-what-s-next.txt:307(搜「Debouncing」) · text/15-ch12-chapter-12-what-s-next.txt:325(搜「Web Workers」) |
| 全书收尾 | 同上 | text/15-ch12-chapter-12-what-s-next.txt:356(搜「resolve successfully」) |
-
出处:「Chapter 12: What's Next?」第 45-55 段(
text/15-ch12-chapter-12-what-s-next.txt:51,搜「filter」;:55,搜「output will be」)。of/filter/map 的例子与输出 4、16 照原书。 ↩ -
出处:同章第 57-86 段(
text/15-ch12-chapter-12-what-s-next.txt:61,搜「forkJoin」;:72,搜「mergeMap」)。取用户→取帖子→合成的管道照原书。 ↩ -
出处:同章第 88-100 段(
text/15-ch12-chapter-12-what-s-next.txt:90,搜「Powerful Abstraction」;:96,搜「Cancellation」;:98,搜「Time-based」)。五个卖点照原书列表。 ↩ -
出处:同章第 112-138 段(
text/15-ch12-chapter-12-what-s-next.txt:114,搜「useEffect」;:138,搜「not allowed」)。async 函数定义在 effect 内再调用的原因照原书。 ↩↩ -
出处:同章第 142-186 段(
text/15-ch12-chapter-12-what-s-next.txt:146,搜「useFetch」)。自定义 hook 返回 { data, loading, error } 照原书。 ↩ -
出处:同章第 192-218 段(
text/15-ch12-chapter-12-what-s-next.txt:192,搜「Watchers」;:213,搜「immediate」)。watcher 触发取数与 immediate 语义照原书。 ↩ -
出处:同章第 220-232 段(
text/15-ch12-chapter-12-what-s-next.txt:222,搜「Async Components」)。loading/error/delay/timeout 四字段照原书。 ↩ -
出处:同章第 236-267 段(
text/15-ch12-chapter-12-what-s-next.txt:240,搜「useFetch」;:255,搜「refresh」)。组合式 useFetch 与 refresh 返回照原书。 ↩ -
出处:同章第 269-348 段(
text/15-ch12-chapter-12-what-s-next.txt:273,搜「Microtasks」;:295,搜「Memory Leaks」;:307,搜「Debouncing」;:319,搜「Caching」;:325,搜「Web Workers」;:344,搜「Profiling」)。五个优化方向照原书。 ↩ -
出处:同章第 9-11 段(
text/15-ch12-chapter-12-what-s-next.txt:11,搜「RxJS」)。"Preparing for Advanced Asynchronous Programming with RxJS"作为本章第一站。 ↩ -
出处:同章第 356 段(
text/15-ch12-chapter-12-what-s-next.txt:356,搜「resolve successfully」)。原文:"Happy coding, and may your Promises always resolve successfully!" ↩ -
出处:「Chapter 12: What's Next?」第 88-100 段(
text/15-ch12-chapter-12-what-s-next.txt:88,搜「Why Learn RxJS」)。"Why Learn RxJS" 的五条卖点照原书;适用边界是反向应用。 ↩ -
出处:「Chapter 12: What's Next?」第 112-138 段(
text/15-ch12-chapter-12-what-s-next.txt:131,搜「userId」)。useEffect 与依赖数组的用法照原书。 ↩ -
出处:「Chapter 12: What's Next?」第 144-186 段(
text/15-ch12-chapter-12-what-s-next.txt:146,搜「useFetch」)。useFetch 返回三态照原书。 ↩ -
出处:「Chapter 12: What's Next?」第 269-348 段(
text/15-ch12-chapter-12-what-s-next.txt:295,搜「Memory Leaks」;:307,搜「Debouncing」)。五个优化方向照原书;优先级是应用层判断。 ↩ -
出处:「Chapter 12: What's Next?」第 9-11 段(
text/15-ch12-chapter-12-what-s-next.txt:9,搜「RxJS」;:102,搜「Frameworks」)。"Preparing for Advanced Asynchronous Programming with RxJS" 与 "Exploring Asynchronous Patterns in Frameworks" 的分节顺序照原书。 ↩ -
出处:「Chapter 12: What's Next?」第 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" 照原书;AsyncContext 方向为补充(不在书里,来自通用知识)。 ↩ -
出处:「Chapter 12: What's Next?」第 192-218 段(
text/15-ch12-chapter-12-what-s-next.txt:203,搜「watch」;:213,搜「immediate」)。watcher 与 immediate 的用法照原书。 ↩ -
出处:「Chapter 12: What's Next?」第 220-232 段(
text/15-ch12-chapter-12-what-s-next.txt:224,搜「AsyncComponent」)。component/loading/error/delay/timeout 五字段照原书。 (本章完:三条进阶路,地基全是前八章。) ↩