React 组件生命周期与并发模式课程设计
🧬 React 组件生命周期与并发模式课程设计
1. 课程概述
本课程旨在系统讲解 React 组件生命周期 和 并发模式(Concurrent Mode) 的核心概念、应用场景及实战技巧。生命周期是组件从创建到销毁的完整过程,理解它能帮助我们在正确时机执行代码;并发模式是 React 18 的革命性特性,它能提升应用响应速度和用户体验。学生将学会如何利用生命周期方法管理组件行为,并运用并发特性构建更流畅的现代应用。
2. 课程目标
| 目标类型 | 描述 |
|---|---|
| 知识目标 | 理解 React 生命周期的三个阶段(挂载、更新、卸载)及每个阶段的常用方法。理解 Concurrent Mode 的设计理念、核心特性(时间切片、优先级调度、可中断渲染)及其对生命周期的影响。掌握 Hooks 如何模拟类组件的生命周期功能。理解 Error Boundary 的工作原理。 |
| 技能目标 | 能在类组件的生命周期方法中执行正确的操作(如数据请求、订阅、清理)。能使用 Hooks(useEffect, useLayoutEffect等) 在函数组件中管理副作用和生命周期逻辑。能使用 useTransition 和 useDeferredValue 标记非紧急更新,优化用户体验。能创建 Error Boundary 组件捕获并处理错误。 |
3. 核心知识点与实例代码
3.1 React 生命周期基础
React 组件的生命周期可分为三个主要阶段:挂载(Mounting)、更新(Updating) 和 卸载(Unmounting),此外还有 错误处理 阶段。
类组件的生命周期方法及实战应用
| 阶段 | 方法 | 触发时机与用途 | 代码示例 |
|---|---|---|---|
| 挂载 | constructor(props) |
组件实例化时调用,用于初始化 state 和绑定事件方法。 | jsx class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); } // ... } |
static getDerivedStateFromProps(props, state) |
在渲染前调用,用于根据 props 的变化来更新 state。适用于少数罕见用例。 | jsx static getDerivedStateFromProps(props, state) { if (props.defaultValue !== state.value) { return { value: props.defaultValue }; } return null; } |
|
render() |
必须实现的方法,负责返回要渲染的 React 元素(JSX)。 | jsx render() { return <div>{this.state.count}</div>; } |
|
componentDidMount() |
组件挂载到 DOM 后立即调用。是进行数据请求、事件订阅、DOM 操作的理想位置。 | jsx componentDidMount() { this.fetchData(); // 数据请求 window.addEventListener('resize', this.handleResize); // 事件订阅 this.timer = setInterval(() => { this.tick(); }, 1000); // 设置定时器 } |
|
| 更新 | static getDerivedStateFromProps(props, state) |
同挂载阶段。 | |
shouldComponentUpdate(nextProps, nextState) |
在渲染前调用,通过返回 true/false 来决定组件是否重新渲染,是性能优化的重要手段。 | ```jsx shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id | |
render() |
同挂载阶段。 | ||
getSnapshotBeforeUpdate(prevProps, prevState) |
在 DOM 更新前调用,用于捕获更新前的 DOM 信息(如滚动位置)。返回值将作为参数传递给 componentDidUpdate。 |
jsx getSnapshotBeforeUpdate(prevProps, prevState) { if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; } |
|
componentDidUpdate(prevProps, prevState, snapshot) |
在更新完成后立即调用。适合执行需要操作 DOM、网络请求等副作用操作。 | jsx componentDidUpdate(prevProps, prevState, snapshot) { if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } } |
|
| 卸载 | componentWillUnmount() |
在组件卸载及销毁之前调用。在此执行必要的清理工作,如清除定时器、取消网络请求、清理订阅等,以防内存泄漏。 | jsx componentWillUnmount() { clearInterval(this.timer); window.removeEventListener('resize', this.handleResize); this.controller.abort(); // 取消未完成的网络请求 } |
| 错误处理 | static getDerivedStateFromError(error) |
在后代组件抛出错误后调用。接收错误信息,并返回一个值以更新 state,从而渲染降级/备用的 UI。 | jsx static getDerivedStateFromError(error) { return { hasError: true }; } |
componentDidCatch(error, errorInfo) |
在后代组件抛出错误后调用。用于记录错误信息(如上报至监控系统)。 | jsx componentDidCatch(error, errorInfo) { logErrorToService(error, errorInfo); } |
函数组件的“生命周期”与 Hooks
函数组件本身没有生命周期方法,但可以通过 React Hooks 来模拟类组件生命周期的功能:
| 类组件生命周期 | 函数组件中的 Hooks 等效实现 | 代码示例 |
|---|---|---|
constructor |
useState 初始化 state |
jsx const [count, setCount] = useState(0); |
componentDidMount |
useEffect(fn, []) |
jsx useEffect(() => { // 相当于 componentDidMount fetchData(); }, []); |
componentDidUpdate |
useEffect(fn, [deps]) |
jsx useEffect(() => { // 在 count 变化后执行(包括首次挂载) console.log('Count updated:', count); }, [count]); |
componentWillUnmount |
useEffect 的清理函数 |
jsx useEffect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); // 清理函数 }, []); |
shouldComponentUpdate |
React.memo, useMemo |
jsx const MyComponent = React.memo(({ value }) => { return <div>{value}</div>; }); |
getSnapshotBeforeUpdate & componentDidUpdate |
useLayoutEffect |
jsx useLayoutEffect(() => { // 在 DOM 更新后同步执行,可用于读取布局并触发重渲染 const { height } = node.getBoundingClientRect(); setHeight(height); }); |
综合示例:数据获取与清理(类组件 vs 函数组件)
// 类组件版本
class UserProfileClass extends React.Component {
state = { user: null, loading: true };
controller = new AbortController();
async fetchUser() {
try {
const response = await fetch(`/api/users/${this.props.userId}`, { signal: this.controller.signal });
const user = await response.json();
this.setState({ user, loading: false });
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
}
}
componentDidMount() {
this.fetchUser();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.setState({ loading: true });
this.fetchUser();
}
}
componentWillUnmount() {
this.controller.abort();
}
render() {
if (this.state.loading) return <div>Loading...</div>;
return <div>{this.state.user.name}</div>;
}
}
// 函数组件版本
function UserProfileFunctional({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`, { signal: controller.signal });
const userData = await response.json();
setUser(userData);
setLoading(false);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
}
};
fetchUser();
return () => {
controller.abort();
};
}, [userId]); // 依赖项:当 userId 变化时,重新执行 effect
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
3.2 React 18 并发模式 (Concurrent Mode)
并发模式是 React 18 引入的一种新的底层架构,它使 React 能够同时准备多个版本的 UI,并可以中断渲染过程以处理更紧急的任务(如用户输入),从而提升应用的响应速度和用户体验。
并发模式的核心特性
- 时间切片 (Time Slicing):将长时间的渲染任务拆分成多个小块(通常为 5ms),在浏览器的空闲期执行,避免阻塞主线程,保证用户交互的流畅性。
- 可中断渲染 (Interruptible Rendering):渲染过程可以被更高优先级的更新中断。React 会暂停当前正在进行的渲染,先去处理更紧急的更新(如用户输入),然后再回来继续或重新开始之前的渲染。
- 优先级调度 (Lane Priority):React 为不同的更新分配了不同的优先级(例如:用户输入 > 动画更新 > 数据加载),确保高优先级的更新能更快地被处理并呈现在屏幕上。
并发模式下的 API 与实战
1. useTransition:标记非紧急更新useTransition 允许你将一个更新标记为非紧急(低优先级),这样它就不会阻塞更紧急的更新(如用户输入或动画)。
import { useState, useTransition } from 'react';
function SearchBox() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition(); // 返回一个待定状态标记和一个启动过渡函数的函数
const handleChange = (e) => {
const value = e.target.value;
setKeyword(value); // 紧急更新:立即更新输入框的值
startTransition(() => { // 非紧急更新:搜索结果可以稍后更新
setResults(prevResults => {
// 假设这是一个昂贵的过滤操作
return prevResults.filter(item => item.name.includes(value));
});
});
};
return (
<div>
<input type="text" value={keyword} onChange={handleChange} />
{/* isPending 表示过渡更新是否正在进行 */}
{isPending ? <span>Loading...</span> : null}
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
2. useDeferredValue:延迟更新某个值useDeferredValue 接受一个值并返回该值的延迟版本。在紧急更新发生时,延迟版本的值会“滞后”于原始值,从而避免阻塞紧急更新。
import { useState, useDeferredValue, useMemo } from 'react';
function ProductList({ products }) {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter); // deferredFilter 会“滞后”于 filter
const filteredList = useMemo(() => {
return products.filter(product =>
product.name.toLowerCase().includes(deferredFilter.toLowerCase())
);
}, [products, deferredFilter]); // 计算依赖于 deferredFilter
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)} // 紧急更新:输入框立即响应
placeholder="Search products..."
/>
{/* 列表渲染使用延迟后的值,可能会稍晚更新 */}
<ul>
{filteredList.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
3. Suspense 与并发模式Suspense 允许组件在等待异步数据加载时**“暂停”渲染**并显示一个回退内容(如加载指示器)。在并发模式下,Suspense 可以更好地与可中断渲染结合。
import { Suspense } from 'react';
function App() {
return (
<div>
<Suspense fallback={<div>Loading User Profile...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Loading Product List...</div>}>
<ProductList />
</Suspense>
</div>
);
}
// 假设 UserProfile 和 ProductList 内部使用了一些异步数据获取库(如 Relay)或实现了 Suspense 的数据获取方式
3.3 错误边界 (Error Boundary)
错误边界是类组件,它捕获并处理其子组件树中发生的 JavaScript 错误,记录这些错误,并显示一个降级 UI 而不是崩溃的组件树。
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.logErrorToService(error, errorInfo); // 上报错误到监控服务
}
logErrorToService(error, info) {
// 这里通常会上报到像 Sentry、LogRocket 这样的错误监控服务
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
</details>
<button onClick={() => this.setState({ hasError: false })}>Try again?</button>
</div>
);
}
return this.props.children;
}
}
// 使用
function App() {
return (
<ErrorBoundary>
<MyComponentWhichMightThrow />
</ErrorBoundary>
);
}
4. 综合实战案例:并发模式下的图片画廊
下面是一个融合了生命周期管理、并发模式特性和错误处理的综合示例:
import React, { useState, useEffect, useTransition, useDeferredValue, Suspense } from 'react';
// 一个模拟异步加载图片的组件,可能会抛出错误
function AsyncImage({ src, alt }) {
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => setIsLoaded(true);
img.onerror = () => setError(new Error(`Failed to load image: ${src}`));
return () => {
img.onload = null;
img.onerror = null;
};
}, [src]);
if (error) throw error; // 抛出错误让 Error Boundary 捕获
if (!isLoaded) return <div className="image-placeholder">Loading...</div>;
return <img src={src} alt={alt} />;
}
// 主组件
function ImageGallery() {
const [query, setQuery] = useState('');
const [images, setImages] = useState([]);
const [isPending, startTransition] = useTransition();
const deferredQuery = useDeferredValue(query);
// 根据 deferredQuery 过滤图片(非紧急计算)
const filteredImages = React.useMemo(() => {
return images.filter(image =>
image.tags.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [images, deferredQuery]);
// 组件挂载时获取图片数据
useEffect(() => {
const fetchImages = async () => {
try {
// 模拟 API 调用
const response = await fetch('/api/images');
const data = await response.json();
setImages(data);
} catch (error) {
console.error('Failed to fetch images:', error);
}
};
fetchImages();
}, []);
const handleSearchChange = (e) => {
const value = e.target.value;
setQuery(value); // 紧急更新:立即更新输入框
// 标记接下来的状态更新为非紧急
startTransition(() => {
// 这里可以设置一些依赖于搜索词的衍生状态
// 但 filteredImages 的计算已经由 useDeferredValue 和 useMemo 处理
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleSearchChange}
placeholder="Search images by tags..."
/>
{isPending && <span className="search-indicator"> (Searching...) </span>}
<div className="image-grid">
<Suspense fallback={<div>Loading gallery...</div>}>
{filteredImages.map(image => (
<div key={image.id} className="image-item">
<ErrorBoundary fallback={<div>Could not load this image.</div>}>
<AsyncImage src={image.url} alt={image.title} />
<p>{image.title}</p>
</ErrorBoundary>
</div>
))}
</Suspense>
</div>
</div>
);
}
// 简化版的 Error Boundary(实际应用中可能更复杂)
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.log('Error caught by Image Error Boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
案例技能点解析:
- 生命周期管理:
useEffect用于数据获取(componentDidMount),AsyncImage 内部的useEffect用于图片加载和清理(componentWillUnmount)。 - 并发模式:
useTransition和useDeferredValue共同协作,确保搜索输入框的响应是即时且流畅的,而昂贵的过滤操作和图片渲染可以被延迟或中断。 - 错误处理:
ErrorBoundary类组件捕获子组件(AsyncImage)中抛出的错误,防止整个画廊因一张图片加载失败而崩溃。 - Suspense:用于包裹整个图片列表,提供加载中的回退UI。
5. 课后练习与思考
-
基础练习:
- 将一个使用
componentDidMount和componentWillUnmount的类组件改写为使用useEffect的函数组件。 - 使用
useTransition优化一个按钮点击后需要执行大量计算的状态更新。
- 将一个使用
-
进阶挑战:
- 实现一个自定义 Hook
useFetch,它接受一个 URL,并返回data、loading和error状态,并在组件卸载时自动取消未完成的请求。 - 构建一个复杂的列表页面,集成搜索(使用
useDeferredValue)、过滤、排序功能,并确保用户交互始终流畅。
- 实现一个自定义 Hook
-
思考题:
- 在并发模式下,
componentDidMount和componentDidUpdate等生命周期方法的行为可能会发生什么变化?开发者需要注意什么? - 为什么说
useLayoutEffect更适合处理 DOM 测量和动画?它与useEffect在执行时机上有何不同? - 错误边界为什么不能捕获其自身内部的错误、异步代码中的错误以及事件处理函数中的错误?
- 在并发模式下,
更多推荐



所有评论(0)