vault backup: 2026-04-29 23:36:32
This commit is contained in:
+106
-15
@@ -7,7 +7,11 @@ create time: 2026-04-29 22:12
|
||||
|
||||
## 概述
|
||||
|
||||
React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为可中断、可恢复、可优先级调度的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。
|
||||
React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为**可中断、可恢复、可优先级调度**的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。
|
||||
|
||||
> [!question] 思考:如果一次 setState 触发了大量 DOM 更新,为什么页面不会卡死?
|
||||
>
|
||||
> 答案就在 React 的 Fiber 架构里——它将渲染工作切分成小单元,每个单元完成后让出主线程给浏览器处理高优任务(如点击响应)。这就是「并发」的本质。
|
||||
|
||||
## React 渲染架构演进
|
||||
|
||||
@@ -37,6 +41,28 @@ const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### Suspense + SSR(流式渲染)
|
||||
|
||||
> [!tip] React 18 的 Streaming SSR
|
||||
>
|
||||
> Suspense 在 SSR 中的核心价值:服务端可以「分块」返回 HTML,用户先看到首屏骨架,数据就绪后逐步替换。这比传统的「等所有数据都就绪再输出完整 HTML」快得多。
|
||||
|
||||
```tsx
|
||||
// 服务端组件中嵌套 Suspense boundary
|
||||
export default async function Page() {
|
||||
return (
|
||||
<main>
|
||||
<Header /> {/* 同步渲染 */}
|
||||
<Suspense fallback={<SearchSkeleton />}>
|
||||
<SearchResults /> {/* 等待 fetch 完成后插入 */}
|
||||
</Suspense>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
服务端输出变成**渐进式 HTML 流**:`Header → SearchSkeleton → SearchResults(loaded)`
|
||||
|
||||
### Suspense + Data Fetching(实验性 API)
|
||||
|
||||
```tsx
|
||||
@@ -60,6 +86,10 @@ function Profile() {
|
||||
|
||||
## useTransition —— 标记低优先级更新
|
||||
|
||||
> [!question] 什么时候该用 Transition?
|
||||
>
|
||||
> 当一次用户交互(如点击、选择)会触发多个状态更新,其中某些更新的 UI 渲染代价高昂时——你可以把「即时反馈」和「延迟渲染」分开。
|
||||
|
||||
```tsx
|
||||
function SearchPage() {
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -88,16 +118,40 @@ function SearchPage() {
|
||||
}
|
||||
```
|
||||
|
||||
### Transition 与直接 setState 对比
|
||||
### useTransition + Suspense 组合模式
|
||||
|
||||
```mermaid
|
||||
timeline
|
||||
title "输入 "hello" 的渲染行为"
|
||||
|
||||
直接 setState : 每次按键 → re-render\n(h/h/e/l/o 共 5 次)
|
||||
useTransition : h,e,l,l → 跳过中间\no → 最终渲染一次
|
||||
两者配合可以实现更精细的加载策略:Transition 控制哪个更新「可以等待」,Suspense 在数据就绪后展示最终内容。
|
||||
|
||||
```tsx
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const deferredTab = useDeferredValue(activeTab);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
|
||||
{isPending && <PageSkeleton />}
|
||||
<Suspense fallback={<SectionLoading />}>
|
||||
<ActiveSection tab={deferredTab} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Transition vs DeferredValue 选择指南
|
||||
|
||||
| 场景 | 推荐 | 原因 |
|
||||
|------|------|------|
|
||||
| 路由切换后展示新视图 | `useTransition` | 明确标记整页过渡状态,配合 `<Suspense>` |
|
||||
| 输入框即时搜索 + 延迟加载 | `useDeferredValue` | 一行搞定,自动处理防抖逻辑 |
|
||||
| 多个状态联动更新 | `useTransition` | 更精确控制哪些 setState 属于低优先级 |
|
||||
| 简单延迟一个值(如过滤条件) | `useDeferredValue` | 侵入性最小,不改变组件交互模式 |
|
||||
| Tab 切换时内容区域的异步加载 | 两者组合 | `startTransition` 控制页面级 Pending,`useDeferredValue` 控制数据流 |
|
||||
|
||||
> [!tip] 核心区别一句话
|
||||
>
|
||||
> - **`useTransition`**:控制**「哪个 setState」**是低优先级的 → 你直接包裹需要降级的那段 setState 调用。
|
||||
> - **`useDeferredValue`**:控制**「哪个值」**是延迟更新的 → 你用 `const delayed = useDeferredValue(value)` 拿到一个滞后 ~16ms 的副本。
|
||||
|
||||
## useDeferredValue —— 延迟副本
|
||||
|
||||
```tsx
|
||||
@@ -117,14 +171,9 @@ function TodoApp() {
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] Transition vs DeferredValue 选择指南
|
||||
> [!note] 内部机制
|
||||
>
|
||||
> | 场景 | 推荐 |
|
||||
> |------|------|
|
||||
> | 表单提交后展示新视图 | `useTransition` |
|
||||
> | 输入框即时搜索 + 延迟加载 | `useDeferredValue` |
|
||||
> | 多个状态联动更新 | `useTransition`(更明确控制粒度) |
|
||||
> | 简单延迟一个值 | `useDeferredValue`(一行搞定) |
|
||||
> `useDeferredValue` 内部等价于一次 `startTransition`。调用后 React 立即用旧值渲染,然后调度一次低优先级的 re-render 来更新为新值。你可以理解为「自动防抖 + 优先级降级」。
|
||||
|
||||
## 时间切片原理
|
||||
|
||||
@@ -174,4 +223,46 @@ useEffect(() => {
|
||||
// 用于发现不纯的 render 函数和清理逻辑缺失
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
> [!tip] 实战经验总结
|
||||
|
||||
### 代码示例:安全的 Effect Cleanup
|
||||
|
||||
```tsx
|
||||
// ❌ 问题:异步 render → effect 执行时 dep 可能已变化
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetchData(url, { signal: controller.signal }).then(setData);
|
||||
return () => controller.abort(); // cleanup 依赖的 url 可能已过时
|
||||
}, [dep]);
|
||||
|
||||
// ✅ 推荐:用 ref 缓存最新值,保证 cleanup 拿到正确的信号
|
||||
const latestSignalRef = useRef<AbortController>();
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
latestSignalRef.current = controller;
|
||||
|
||||
fetchData(url, { signal: controller.signal }).then(setData);
|
||||
|
||||
return () => {
|
||||
if (latestSignalRef.current === controller) {
|
||||
controller.abort(); // 确认是同一个请求才 abort
|
||||
}
|
||||
};
|
||||
}, [dep]);
|
||||
```
|
||||
|
||||
### 要点清单
|
||||
|
||||
1. **优先使用 Suspense,谨慎手动 startTransition**:Suspense 配合数据获取模式是 React 官方推荐的方向,而 `startTransition` 适合「一个交互触发多个 setState」的场景。
|
||||
2. **避免在 render 中做副作用**:并发模式下 render 函数可能被多次调用、随时中断——render 应该是纯粹的「视图描述函数」。
|
||||
3. **合理使用 `<Suspense>` boundary 层级**:不要把所有组件包在一个大 Suspense 里,也不要每个小组件都加——根据网络请求和数据依赖划分边界。
|
||||
4. **用 `use()` + Suspense 替代 useEffect 中的数据获取**:实验性但代表未来方向,能消除竞态条件(race condition)和 loading 状态管理样板代码。
|
||||
5. **警惕 `useEffect` 的异步执行时机**:如果 effect 依赖于某个 state 的值来做 cleanup,考虑改用 `useSyncExternalStore` 或使用 ref 存储最新值。
|
||||
|
||||
## 关联笔记
|
||||
- [[11-组件通信模式]]
|
||||
- [[12-HOC 与 Render Props]]
|
||||
- [[14-Serverside Rendering]]
|
||||
|
||||
Reference in New Issue
Block a user