Files
cs-note/hhs/REACT/4. 进阶篇/13-并发特性.md
T
2026-05-24 11:42:38 +08:00

269 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, Concurrent, Suspense, Transitions, Frontend]
create time: 2026-04-29 22:12
---
# 并发特性
## 概述
React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为**可中断、可恢复、可优先级调度**的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。
> [!question] 思考:如果一次 setState 触发了大量 DOM 更新,为什么页面不会卡死?
>
> 答案就在 React 的 Fiber 架构里——它将渲染工作切分成小单元,每个单元完成后让出主线程给浏览器处理高优任务(如点击响应)。这就是「并发」的本质。
## React 渲染架构演进
```mermaid
graph LR
A["React 17 同步渲染"] -->|"全部一次性完成"| B["长时间阻塞主线程 ❌"]
C["React 18 并发渲染"] -->|"可打断/可恢复"| D["Fiber Scheduler"]
D --> E["高优先级任务:用户输入 ⏩"]
D --> F["低优先级任务:数据加载 🐢"]
F -->|"被高优先级打断"| G["暂停 → 之后恢复 ✅"]
style B fill:#F5A87D,color:#000
style G fill:#4FC08D,color:#fff
```
## Suspense —— 声明式等待
### 基本用法
```tsx
// LazyComponent 在首次挂载时自动 code-split
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
<Suspense fallback={<PageSkeleton />}>
<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
import { use } from "react";
// 资源标记为 Suspense-compatible
const promise = fetchData("/api/profile");
function Profile() {
// use() 会 suspend 直到 promise resolve
const profile = use(promise);
return <div>{profile.name}</div>;
}
// 外层用 Suspense 包裹
<Suspense fallback={<ProfileLoading />}>
<Profile />
</Suspense>
```
## useTransition —— 标记低优先级更新
> [!question] 什么时候该用 Transition?
>
> 当一次用户交互(如点击、选择)会触发多个状态更新,其中某些更新的 UI 渲染代价高昂时——你可以把「即时反馈」和「延迟渲染」分开。
```tsx
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Result[]>([]);
const [isPending, startTransition] = useTransition();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// 立即响应输入
setQuery(value);
// 低优先级的结果更新(可被输入打断)
startTransition(() => {
setResults(heavySearch(value));
});
};
return (
<>
<input value={query} onChange={handleInputChange} />
{isPending && <Spinner />}
<Results list={results} />
</>
);
}
```
### useTransition + Suspense 组合模式
两者配合可以实现更精细的加载策略: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
function TodoApp() {
const [filter, setFilter] = useState("");
const deferredFilter = useDeferredValue(filter); // 延迟 ~16ms
return (
<>
{/* 快速响应用户输入 */}
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="过滤..." />
{/* 耗时操作使用延迟值 */}
<TodoList todos={allTodos} filter={deferredFilter} />
</>
);
}
```
> [!note] 内部机制
>
> `useDeferredValue` 内部等价于一次 `startTransition`。调用后 React 立即用旧值渲染,然后调度一次低优先级的 re-render 来更新为新值。你可以理解为「自动防抖 + 优先级降级」。
## 时间切片原理
```mermaid
sequenceDiagram
participant Browser as 浏览器主线程
participant Fiber as Fiber Scheduler
Browser->>Fiber: 构建整棵组件树(工作单元)
Fiber->>Browser: 渲染帧 1(约 16ms)✅
Note over Fiber,Browser: 把任务切分为 ~5ms 的工作单元
Browser->>Fiber: 下一帧开始
Fiber->>Browser: 渲染帧 2 ✅
Note over Fiber: 如果用户点击了按钮<br/>高优先级任务插入队列
Fiber-->>Browser: 暂停当前帧
Browser->>Fiber: 处理用户输入 ✅
Fiber->>Browser: 继续未完成的渲染帧
style Browser fill:#F5A87D,color:#000
style Fiber fill:#4FC08D,color:#fff
```
### 关键概念
- **Work Breakdown**:React 将渲染工作拆成多个小单元(work unit),每个单元约 1ms
- **Yield to Browser**:每个单元完成后让出控制权给浏览器,确保 UI 响应性
- **Priority Scheduling**:用户输入 > 网络响应 > 后台数据更新 > 动画
## 并发模式下的陷阱
> [!warning] 需要注意的行为变化
```tsx
// 1. useEffect 的执行时机变了
useEffect(() => {
console.log("effect");
}, [dep]);
// 效果不再与渲染同步,可能在异步 commit 阶段执行
// 2. 浏览器生命周期方法(如 getSnapshotBeforeUpdate)已过时
// 并发模式下无法保证同步 commit
// 3. 旧版第三方库可能与并发不兼容
// 遇到 "Cannot update a component during rendering" → 检查是否有不规范的 side effect
// 4. StrictMode 双重渲染(开发环境)
// 用于发现不纯的 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]]