跳到主要内容

React 生命周期演变

问题

React 16/17/18/19 各版本的生命周期有什么变化?为什么要废弃某些生命周期?

答案

React 生命周期经历了重大演变:从类组件的生命周期方法,到函数组件的 Hooks,再到并发模式下的新范式。

版本演变概览

React 16.3:生命周期改革

废弃的生命周期

React 16.3 废弃了三个生命周期方法(17 版本正式移除):

废弃方法替代方案废弃原因
componentWillMountconstructor + componentDidMount服务端渲染问题、Fiber 可能多次调用
componentWillReceivePropsgetDerivedStateFromProps常见错误用法、副作用问题
componentWillUpdategetSnapshotBeforeUpdateFiber 可能中断、多次调用
为什么废弃?

React Fiber 架构引入后,render 阶段可能被中断和重新执行。这意味着 componentWillMountcomponentWillUpdate 可能被调用多次,如果在这些方法中执行副作用(如网络请求、订阅),会导致问题。

新增的生命周期

getDerivedStateFromProps

静态方法,根据 props 派生 state:

class Example extends React.Component<Props, State> {
// 静态方法,无法访问 this
static getDerivedStateFromProps(
nextProps: Props,
prevState: State
): Partial<State> | null {
// 根据 props 计算新的 state
if (nextProps.value !== prevState.prevValue) {
return {
derivedValue: computeValue(nextProps.value),
prevValue: nextProps.value,
};
}
return null; // 不更新 state
}
}
使用建议

大多数情况下不需要 getDerivedStateFromProps,考虑以下替代方案:

  1. 完全受控组件:让父组件管理状态
  2. key 重置组件:通过更换 key 重置内部状态
  3. useMemo:在函数组件中派生状态

getSnapshotBeforeUpdate

在 DOM 更新前捕获信息:

class ScrollingList extends React.Component<Props, State> {
private listRef = React.createRef<HTMLUListElement>();

getSnapshotBeforeUpdate(prevProps: Props, prevState: State): number | null {
// 捕获更新前的滚动位置
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list ? list.scrollHeight - list.scrollTop : null;
}
return null;
}

componentDidUpdate(
prevProps: Props,
prevState: State,
snapshot: number | null
) {
// 使用 snapshot 恢复滚动位置
if (snapshot !== null) {
const list = this.listRef.current;
if (list) {
list.scrollTop = list.scrollHeight - snapshot;
}
}
}

render() {
return <ul ref={this.listRef}>{/* ... */}</ul>;
}
}

React 16.3 完整生命周期

React 16.8:Hooks 革命

Hooks 让函数组件拥有状态和生命周期能力,彻底改变了 React 开发方式。

生命周期映射

类组件生命周期Hooks 等效实现
constructoruseState 初始化
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {})
componentWillUnmountuseEffect 返回的清理函数
shouldComponentUpdateReact.memo + useMemo
getDerivedStateFromProps渲染时直接计算或 useMemo
getSnapshotBeforeUpdateuseLayoutEffect(部分场景)

useEffect 统一生命周期

function Example({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
// componentDidMount + componentDidUpdate
console.log('组件挂载或 userId 变化');

let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) setUser(data);
});

// componentWillUnmount + 清理上一次副作用
return () => {
console.log('清理');
cancelled = true;
};
}, [userId]); // 依赖数组

return <div>{user?.name}</div>;
}

useLayoutEffect vs useEffect

// useEffect: 异步执行,不阻塞绘制
useEffect(() => {
// 浏览器绘制后执行
// 适合:数据获取、订阅、日志
}, []);

// useLayoutEffect: 同步执行,阻塞绘制
useLayoutEffect(() => {
// DOM 更新后、浏览器绘制前执行
// 适合:DOM 测量、滚动位置、同步 DOM 操作
}, []);

React 17:过渡版本

React 17 是一个"垫脚石"版本,没有新特性,主要为渐进式升级打基础。

主要变化

变化说明
事件委托位置document 改为 rootNode
移除事件池event 对象不再复用
新 JSX 转换无需手动 import React
移除私有 API清理内部 API

事件委托变化

// React 16: 事件绑定到 document
document.addEventListener('click', handler);

// React 17+: 事件绑定到 root 元素
rootNode.addEventListener('click', handler);

这个变化使得多个 React 版本可以共存在同一页面。

移除事件池

// React 16: 需要 e.persist()
function handleClick(e: React.MouseEvent) {
e.persist(); // 阻止事件对象被复用
setTimeout(() => {
console.log(e.target); // 之前不 persist 会是 null
}, 100);
}

// React 17+: 不再需要
function handleClick(e: React.MouseEvent) {
setTimeout(() => {
console.log(e.target); // 正常工作
}, 100);
}

React 18:并发时代

React 18 引入并发渲染,带来了生命周期行为的重要变化。

Strict Mode 下 useEffect 执行两次

在开发模式的 Strict Mode 下,React 18 会模拟组件卸载和重新挂载

function Example() {
useEffect(() => {
console.log('effect 执行'); // 开发模式下打印两次

return () => {
console.log('cleanup 执行'); // 第一次挂载后立即执行
};
}, []);

return <div>Example</div>;
}

// 开发模式输出:
// effect 执行
// cleanup 执行
// effect 执行
为什么要执行两次?

这是为了帮助开发者发现副作用中的问题,确保组件能正确处理:

  1. 卸载和重新挂载(如 Fast Refresh)
  2. 并发模式下的中断和恢复
  3. 离屏渲染(Offscreen)

解决方案:确保 cleanup 函数正确清理副作用。

新增 Hooks

// useId: 生成稳定的唯一 ID(SSR 安全)
function FormField() {
const id = useId();
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} />
</>
);
}

// useSyncExternalStore: 订阅外部 store
const state = useSyncExternalStore(
subscribe, // 订阅函数
getSnapshot, // 获取客户端快照
getServerSnapshot // SSR 快照(可选)
);

// useInsertionEffect: CSS-in-JS 库使用
useInsertionEffect(() => {
// 在 DOM 变更前注入样式
// 比 useLayoutEffect 更早执行
}, []);

执行顺序对比

并发特性对生命周期的影响

function SearchResults({ query }: { query: string }) {
// useTransition: 非紧急更新可能被中断
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState<string[]>([]);

useEffect(() => {
// 在并发模式下,这个 effect 可能会:
// 1. 被延迟执行
// 2. 在组件渲染被中断后不执行
startTransition(() => {
fetchResults(query).then(setResults);
});
}, [query]);

return (
<div>
{isPending && <Spinner />}
{results.map(r => <div key={r}>{r}</div>)}
</div>
);
}

React 19:编译器时代

React 19 引入 React Compiler,改变了性能优化的范式。

主要变化

特性说明
React Compiler自动 memoization,减少手动优化
Actions新的数据变更范式
use()可条件调用的 Hook
ref 作为 prop无需 forwardRef
文档元数据内置 <title><meta> 支持

useEffect 与 Actions

// React 18: useEffect 处理副作用
function Form() {
const [isPending, setIsPending] = useState(false);

async function handleSubmit(formData: FormData) {
setIsPending(true);
try {
await submitForm(formData);
} finally {
setIsPending(false);
}
}

return <form onSubmit={e => { e.preventDefault(); handleSubmit(new FormData(e.target)) }} />;
}

// React 19: Actions
function Form() {
const [state, formAction, isPending] = useActionState(submitForm, initialState);

return <form action={formAction}>...</form>;
}

use() 打破 Hooks 规则

// React 19: use() 可以条件调用
function Component({ shouldFetch }: { shouldFetch: boolean }) {
if (shouldFetch) {
const data = use(fetchPromise); // 条件调用!
return <div>{data}</div>;
}
return <div>No data</div>;
}

// 传统 Hooks 不能条件调用
function Component({ shouldFetch }: { shouldFetch: boolean }) {
// ❌ 错误:Hooks 必须顶层调用
// if (shouldFetch) {
// const [data] = useState();
// }
}

生命周期最佳实践

现代 React(Hooks)推荐模式

function ModernComponent({ id }: { id: string }) {
// 1. State 初始化
const [data, setData] = useState<Data | null>(null);
const [error, setError] = useState<Error | null>(null);

// 2. 派生状态:直接计算,不要额外 state
const processedData = data ? processData(data) : null;

// 3. 副作用:数据获取
useEffect(() => {
let cancelled = false;

fetchData(id)
.then(result => {
if (!cancelled) setData(result);
})
.catch(err => {
if (!cancelled) setError(err);
});

return () => { cancelled = true; };
}, [id]);

// 4. 副作用:订阅
useEffect(() => {
const subscription = subscribe(id, setData);
return () => subscription.unsubscribe();
}, [id]);

// 5. DOM 操作:使用 useLayoutEffect
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (ref.current) {
ref.current.scrollTop = 0;
}
}, [data]);

// 6. 渲染
if (error) return <Error error={error} />;
if (!data) return <Loading />;
return <div ref={ref}>{processedData}</div>;
}

避免常见错误

// ❌ 错误:在 effect 中同步状态
useEffect(() => {
setDerivedValue(computeValue(props.value));
}, [props.value]);

// ✅ 正确:直接在渲染时计算
const derivedValue = useMemo(() => computeValue(props.value), [props.value]);

// ❌ 错误:忽略 cleanup
useEffect(() => {
const timer = setInterval(() => {}, 1000);
// 忘记清理,导致内存泄漏
}, []);

// ✅ 正确:始终清理
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => clearInterval(timer);
}, []);

// ❌ 错误:依赖数组不完整
useEffect(() => {
fetchData(userId); // userId 未加入依赖
}, []);

// ✅ 正确:包含所有依赖
useEffect(() => {
fetchData(userId);
}, [userId]);

常见面试问题

Q1: React 16 为什么废弃 componentWillMount 等生命周期?

答案

主要原因是 Fiber 架构的引入:

  1. Render 阶段可中断:Fiber 架构下,render 阶段可能被中断和重新执行,导致 componentWillMountcomponentWillUpdate 可能执行多次

  2. 副作用问题:在这些方法中执行网络请求、订阅等副作用会导致重复执行

  3. 并发模式准备:为 React 18 的并发模式做准备

// ❌ 之前常见的错误用法
componentWillMount() {
// 可能执行多次!
fetchData();
subscribe();
}

// ✅ 正确用法
componentDidMount() {
// 只执行一次
fetchData();
subscribe();
}

Q2: useEffect 和 useLayoutEffect 的区别?

答案

特性useEffectuseLayoutEffect
执行时机浏览器绘制浏览器绘制
是否阻塞异步,不阻塞同步,阻塞绘制
适用场景数据获取、订阅DOM 测量、同步 DOM 操作
// 闪烁问题示例
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const ref = useRef<HTMLDivElement>(null);

// ❌ useEffect: 可能闪烁
useEffect(() => {
const rect = ref.current?.getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}, []);

// ✅ useLayoutEffect: 无闪烁
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect();
setPosition({ x: rect.x, y: rect.y });
}, []);
}

Q3: React 18 Strict Mode 下为什么 useEffect 执行两次?

答案

这是有意为之的行为,目的是帮助发现副作用问题:

  1. 模拟卸载重挂载:确保组件能正确处理这种情况
  2. 发现 cleanup 问题:暴露未正确清理的副作用
  3. 为并发特性准备:并发模式下组件可能被中断

解决方案

useEffect(() => {
const controller = new AbortController();

fetchData({ signal: controller.signal });

// ✅ 正确清理
return () => controller.abort();
}, []);

Q4: 类组件和函数组件生命周期如何对应?

答案

// 类组件
class Example extends React.Component {
constructor(props) {
// → useState 初始化
}
componentDidMount() {
// → useEffect(() => {}, [])
}
componentDidUpdate(prevProps) {
// → useEffect(() => {}, [deps])
}
componentWillUnmount() {
// → useEffect return 的清理函数
}
shouldComponentUpdate() {
// → React.memo
}
}

// 函数组件
function Example(props) {
const [state, setState] = useState(initialState);

useEffect(() => {
// componentDidMount + componentDidUpdate
return () => {
// componentWillUnmount
};
}, [deps]);

return <div />;
}

const MemoizedExample = React.memo(Example); // shouldComponentUpdate

Q5: React 各版本生命周期相关的主要变化?

答案

版本主要变化
16.3废弃 componentWillMount 等,新增 getDerivedStateFromPropsgetSnapshotBeforeUpdate
16.8引入 Hooks,useEffect 统一生命周期
17移除事件池,事件委托改为 root 元素
18并发模式,Strict Mode 下 effect 执行两次,新增 useIduseSyncExternalStoreuseInsertionEffect
19React Compiler 自动优化,Actions 范式,use() 可条件调用

相关链接