深入理解 React 的生命周期及 Hooks 对于构建高效、可维护的应用至关重要。随着 React Hooks 的引入,函数组件不仅具备了类组件的所有功能,还带来了更简洁、可复用和灵活的代码结构。本文将全面解析 React 的生命周期及 Hooks,帮助你深入理解它们的工作原理、应用场景及最佳实践,并与 Vue 3 的生命周期及组合式 API 进行对比,以便 Vue 3 开发者更好地掌握 React 的核心概念。
目录
-
- 2.1 Hooks 概述
- 2.2
useEffect
-
-
- 2.2.1
useEffect
的基本用法 - 2.2.2 清理副作用
- 2.2.3 依赖项数组
- 2.2.1
-
-
- 2.3
useLayoutEffect
- 2.4
useRef
- 2.5 自定义 Hooks
- 2.3
1. React 类组件的生命周期
在 React 中,类组件通过生命周期方法管理组件的不同阶段。生命周期方法分为挂载(Mounting)、更新(Updating)、卸载(Unmounting)和错误处理(Error Handling)四个主要阶段。
1.1 挂载阶段
挂载阶段是指组件被创建并插入到 DOM 中的过程。主要涉及以下生命周期方法:
- constructor(props)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increase</button>
</div>
);
}
}
-
- 用于初始化状态和绑定方法。
- 在组件挂载之前调用一次。
- static getDerivedStateFromProps(props, state)
static getDerivedStateFromProps(props, state) {
if (props.initialCount !== state.count) {
return { count: props.initialCount };
}
return null;
}
-
- 在挂载和更新阶段均会被调用。
- 根据 props 更新 state,通常用于从 props 派生 state。
- render()
-
- 唯一必须实现的方法。
- 返回要渲染的 JSX。
- componentDidMount()
componentDidMount() {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => this.setState({ data }));
}
-
- 在组件挂载到 DOM 后立即调用。
- 适用于数据获取、订阅等副作用操作。
1.2 更新阶段
更新阶段发生在组件的 props 或 state 发生变化时,导致组件重新渲染。涉及以下生命周期方法:
- static getDerivedStateFromProps(props, state)
-
- 同挂载阶段,用于根据新的 props 更新 state。
- shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate(nextProps, nextState) {
return nextState.count !== this.state.count;
}
-
- 控制组件是否需要重新渲染。
- 返回
false
可以阻止更新,优化性能。
- render()
-
- 与挂载阶段相同,负责渲染组件。
- getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.count < this.state.count) {
return this.myRef.scrollHeight;
}
return null;
}
-
- 在 DOM 更新前调用。
- 可用于获取 DOM 状态(如滚动位置),返回值会作为
componentDidUpdate
的第三个参数。
- componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.myRef.scrollTop = snapshot;
}
}
-
- 在组件更新后调用。
- 可用于操作 DOM 或进行进一步的数据请求。
1.3 卸载阶段
卸载阶段是指组件从 DOM 中移除的过程。主要涉及以下生命周期方法:
- componentWillUnmount()
componentWillUnmount() {
clearInterval(this.timer);
}
-
- 在组件卸载前调用。
- 用于清理副作用,如取消订阅、清除定时器等。
1.4 错误处理阶段
错误处理阶段用于捕获渲染过程中、生命周期方法或子组件中的错误。涉及以下生命周期方法:
- static getDerivedStateFromError(error)
static getDerivedStateFromError(error) {
return { hasError: true };
}
-
- 在渲染过程中抛出错误时调用。
- 用于更新 state,以展示回退的 UI。
- componentDidCatch(error, info)
componentDidCatch(error, info) {
logErrorToService(error, info);
}
-
- 捕获错误并执行副作用,如记录错误日志。
2. React 函数组件的生命周期及 Hooks
React Hooks 是 React 16.8 引入的功能,允许在函数组件中使用状态和其他 React 特性。通过 Hooks,函数组件可以管理状态、处理副作用、引用 DOM 元素等,几乎具备了类组件的所有功能。
2.1 Hooks 概述
Hooks 是一组能够让你在函数组件中“钩入” React 特性的函数。主要的 Hooks 包括:
useState
:管理状态。useEffect
:处理副作用。useContext
:消费上下文。useReducer
:管理复杂状态逻辑。useRef
:引用 DOM 元素或存储可变值。useMemo
和useCallback
:性能优化。- 自定义 Hooks:复用组件逻辑。
2.2 useEffect
useEffect
是最常用的 Hook,用于在函数组件中处理副作用,如数据获取、订阅、手动操作 DOM 等。它可以模拟类组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
生命周期方法。
2.2.1 useEffect
的基本用法
示例:
import React, { useState, useEffect } from 'react';
function FetchData({ url }) {
const [data, setData] = useState(null);
useEffect(() => {
// 类似于 componentDidMount 和 componentDidUpdate
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, [url]); // 依赖项数组
if (!data) return <p>Loading...</p>;
return <div>{JSON.stringify(data)}</div>;
}
export default FetchData;
解释:
useEffect
在组件渲染后执行。- 依赖项数组
[url]
表示当url
变化时重新执行副作用。 - 初始挂载和依赖项变化都会触发副作用。
2.2.2 清理副作用
useEffect
可以返回一个清理函数,用于在组件卸载或在下一次副作用执行前清理上一次的副作用。
示例:
import React, { useEffect } from 'react';
function Timer() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
// 清理函数
return () => clearInterval(timer);
}, []); // 空依赖项数组,仅在挂载和卸载时执行
return <div>Timer is running. Check console for ticks.</div>;
}
export default Timer;
解释:
- 副作用在组件挂载时执行,设置一个定时器。
- 返回的清理函数在组件卸载时调用,清除定时器。
2.2.3 依赖项数组
依赖项数组决定了 useEffect
何时执行副作用:
- 无依赖项:每次渲染后都执行副作用。
- 空数组
[]
:仅在挂载和卸载时执行副作用。 - 特定依赖项:仅在依赖项变化时执行副作用。
示例:
useEffect(() => {
// 每次渲染后执行
});
useEffect(() => {
// 仅在挂载和卸载时执行
}, []);
useEffect(() => {
// 依赖项变化时执行
}, [dependency1, dependency2]);
2.3 useLayoutEffect
useLayoutEffect
与 useEffect
类似,但它会在所有 DOM 变更之后、浏览器绘制之前同步执行。这使得它适用于需要同步测量 DOM 或在 DOM 变更前进行某些操作的场景。
示例:
import React, { useLayoutEffect, useRef } from 'react';
function MeasureDiv() {
const divRef = useRef();
useLayoutEffect(() => {
const height = divRef.current.offsetHeight;
console.log('Div height:', height);
}, []);
return <div ref={divRef}>Measure my height!</div>;
}
export default MeasureDiv;
注意:
useLayoutEffect
的执行时机可能会阻塞浏览器绘制,需谨慎使用。- 在大多数情况下,
useEffect
足以满足需求,只有在需要同步读取布局时才使用useLayoutEffect
。
2.4 useRef
useRef
用于在函数组件中创建一个可变的引用,该引用在组件的整个生命周期内保持不变。常用于访问 DOM 元素或存储任何可变值。
示例:
import React, { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
export default TextInput;
用途:
- 访问 DOM 元素:如获取输入框的值或焦点。
- 存储可变值:不引起重新渲染的变量,如定时器 ID。
2.5 自定义 Hooks
自定义 Hooks 允许你将组件逻辑提取到可重用的函数中,增强代码的可复用性和可维护性。它们遵循命名约定,以 use
开头,并且可以组合现有的 Hooks 来实现复杂的逻辑。
示例:
import React, { useState, useEffect } from 'react';
// 自定义 Hook
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 清理函数
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
function DisplayWidth() {
const width = useWindowWidth();
return <p>窗口宽度: {width}px</p>;
}
export default DisplayWidth;
优点:
- 代码复用:在多个组件中复用相同的逻辑。
- 组织逻辑:将相关逻辑聚合在一起,提高代码可读性。
- 测试友好:易于单独测试自定义 Hooks。
3. Hooks 模拟生命周期方法的详解
通过 Hooks,函数组件可以模拟类组件的生命周期方法。理解这种模拟关系有助于更好地运用 Hooks 管理组件生命周期。
3.1 类组件 vs 函数组件
生命周期阶段 | 类组件生命周期方法 | 函数组件 Hooks |
---|---|---|
挂载 | constructor componentDidMount | useEffect (空依赖项数组) |
更新 | componentDidUpdate | useEffect (依赖项变化) |
卸载 | componentWillUnmount | useEffect (清理函数) |
错误 | componentDidCatch getDerivedStateFromError | 无直接对应 Hooks(使用 Error Boundaries) |
3.2 类组件生命周期方法与 Hooks 对比
3.2.1 componentDidMount
vs useEffect
(空依赖项数组)
类组件:
class MyComponent extends React.Component {
componentDidMount() {
// 执行副作用,如数据获取
}
render() {
return <div>My Component</div>;
}
}
函数组件:
import React, { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 执行副作用,如数据获取
}, []); // 空依赖项数组,类似 componentDidMount
return <div>My Component</div>;
}
export default MyComponent;
3.2.2 componentDidUpdate
vs useEffect
(依赖项变化)
类组件:
class MyComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (this.props.value !== prevProps.value) {
// 执行副作用,如更新数据
}
}
render() {
return <div>Value: {this.props.value}</div>;
}
}
函数组件:
import React, { useEffect } from 'react';
function MyComponent({ value }) {
useEffect(() => {
// 执行副作用,如更新数据
}, [value]); // 仅当 value 变化时执行
return <div>Value: {value}</div>;
}
export default MyComponent;
3.2.3 componentWillUnmount
vs useEffect
(清理函数)
类组件:
class MyComponent extends React.Component {
componentDidMount() {
this.timer = setInterval(() => {
// 定时任务
}, 1000);
}
componentWillUnmount() {
clearInterval(this.timer); // 清理定时器
}
render() {
return <div>Timer</div>;
}
}
函数组件:
import React, { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const timer = setInterval(() => {
// 定时任务
}, 1000);
// 清理函数
return () => clearInterval(timer);
}, []); // 空依赖项数组,仅在挂载和卸载时执行
return <div>Timer</div>;
}
export default MyComponent;
3.2.4 错误处理
类组件:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
函数组件:
目前,函数组件无法直接捕获错误。需要使用类组件作为错误边界。
示例:
import React from 'react';
class ErrorBoundary extends React.Component {
// 同上
}
// 使用错误边界
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}
注意: 未来 React 可能会引入 Hooks 来处理错误边界,但目前需要使用类组件。
4. React 生命周期与 Vue 3 生命周期的对比
对于熟悉 Vue 3 的开发者而言,理解 React 生命周期与 Vue 3 生命周期之间的异同,有助于更快地适应 React 的开发模式。
4.1 挂载阶段
React 类组件 | React 函数组件 | Vue 3 |
---|---|---|
constructor | useState 初始化状态 | setup 初始化状态 |
componentDidMount | useEffect (空依赖) | onMounted |
示例(Vue 3):
<script setup>
import { ref, onMounted } from 'vue';
const count = ref(0);
onMounted(() => {
console.log('组件已挂载');
});
</script>
<template>
<div>Count: {{ count }}</div>
</template>
4.2 更新阶段
React 类组件 | React 函数组件 | Vue 3 |
---|---|---|
componentDidUpdate | useEffect (依赖变化) | watch / onUpdated |
示例(Vue 3):
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="count++">Increase</button>
</div>
</template>
4.3 卸载阶段
React 类组件 | React 函数组件 | Vue 3 |
---|---|---|
componentWillUnmount | useEffect (清理函数) | onBeforeUnmount |
示例(Vue 3):
<script setup>
import { ref, onBeforeUnmount } from 'vue';
const timer = ref(null);
timer.value = setInterval(() => {
console.log('Tick');
}, 1000);
onBeforeUnmount(() => {
clearInterval(timer.value);
});
</script>
<template>
<div>Timer is running. Check console for ticks.</div>
</template>
4.4 错误处理
React 类组件 | React 函数组件 | Vue 3 |
---|---|---|
componentDidCatch | 无直接对应 | errorCaptured |
示例(Vue 3):
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['errorCaptured']);
function handleError(error, info) {
emit('errorCaptured', error, info);
}
</script>
<template>
<div>
<!-- 子组件可能触发错误 -->
</div>
</template>
注意: Vue 3 支持在组件内捕获错误并处理,类似于 React 的错误边界。
5. Hooks 的最佳实践
正确使用 Hooks 能提升代码质量、可维护性和性能。以下是一些在 React 函数组件中使用 Hooks 的最佳实践。
5.1 遵循 Hook 规则
- 只在顶层调用 Hooks
-
- 不要在循环、条件或嵌套函数中调用 Hooks。
- 只在 React 函数组件或自定义 Hooks 中调用 Hooks
-
- 不要在普通的 JavaScript 函数中调用 Hooks。
示例:
// 正确
function MyComponent() {
const [count, setCount] = useState(0);
// ...
}
// 错误
function MyComponent() {
if (someCondition) {
const [count, setCount] = useState(0); // 不要在条件语句中调用 Hooks
}
// ...
}
5.2 依赖项的正确管理
- 确保依赖项完整
-
- 在
useEffect
、useCallback
、useMemo
等 Hooks 中,依赖项数组应包含所有在副作用中使用的外部变量。
- 在
- 避免不必要的依赖项
-
- 使用稳定的函数引用,如
useCallback
缓存回调函数,避免因函数引用变化导致副作用频繁执行。
- 使用稳定的函数引用,如
示例:
useEffect(() => {
fetchData(userId);
}, [userId]); // 确保包含所有依赖项
5.3 避免滥用 Hooks
- 不要过度分解 Hooks
-
- 过度分解可能导致代码难以理解和维护。
- 保持 Hooks 简单
-
- 每个 Hook 应专注于单一职责,便于复用和测试。
5.4 使用自定义 Hooks 复用逻辑
- 提取可复用的逻辑到自定义 Hooks
-
- 如数据获取、表单处理等逻辑,可以通过自定义 Hooks 在多个组件中复用。
示例:
// useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
fetch(url)
.then(res => res.json())
.then(data => {
if (isMounted) {
setData(data);
setLoading(false);
}
});
return () => { isMounted = false };
}, [url]);
return { data, loading };
}
export default useFetch;
// DataDisplay.js
import React from 'react';
import useFetch from './useFetch';
function DataDisplay({ url }) {
const { data, loading } = useFetch(url);
if (loading) return <p>Loading...</p>;
return <div>Data: {JSON.stringify(data)}</div>;
}
export default DataDisplay;
6. 常见问题与误区
6.1 useEffect
中的依赖项误用
问题:
- 遗漏依赖项:可能导致副作用未能正确响应依赖项变化。
- 不必要的依赖项:可能导致副作用频繁执行,影响性能。
解决方案:
- 严格遵循 ESLint 插件:使用
eslint-plugin-react-hooks
检查依赖项。 - 使用稳定的函数引用:通过
useCallback
缓存函数,避免因函数引用变化导致副作用频繁执行。
示例:
useEffect(() => {
doSomething(count);
}, [count]); // 确保包含所有依赖项
6.2 多个 useEffect
的执行顺序
理解:
- React 会按照 Hooks 的调用顺序依次执行所有的
useEffect
。 - 每个
useEffect
的执行顺序与其在代码中的位置一致。
示例:
function MyComponent() {
useEffect(() => {
console.log('Effect 1');
}, []);
useEffect(() => {
console.log('Effect 2');
}, []);
return <div>Check console</div>;
}
// 控制台输出顺序:
// Effect 1
// Effect 2
6.3 useLayoutEffect
的使用场景
理解:
useLayoutEffect
在 DOM 更新后、浏览器绘制前同步执行,适用于需要读取布局并同步触发重渲染的场景。- 不建议在性能敏感的应用中过度使用
useLayoutEffect
,以避免阻塞浏览器绘制。
示例:
import React, { useLayoutEffect, useRef, useState } from 'react';
function LayoutEffectExample() {
const divRef = useRef();
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
const newHeight = divRef.current.offsetHeight;
setHeight(newHeight);
}, []);
return (
<div>
<div ref={divRef} style={{ height: '100px', background: 'lightblue' }}>
Measured Div
</div>
<p>Div Height: {height}px</p>
</div>
);
}
export default LayoutEffectExample;
7. 总结
React 的生命周期管理通过类组件的生命周期方法和函数组件的 Hooks 提供了强大的功能,允许开发者在不同阶段执行特定的操作。理解并正确使用这些生命周期方法和 Hooks 是构建高效、可维护 React 应用的关键。
关键要点:
- 类组件生命周期方法:包括挂载、更新、卸载和错误处理四个阶段。
- 函数组件 Hooks:通过
useEffect
、useLayoutEffect
、useRef
等 Hooks 实现生命周期管理。 - Hooks 的规则:只在顶层调用 Hooks,只在 React 函数组件或自定义 Hooks 中调用 Hooks。
- 依赖项管理:确保
useEffect
的依赖项数组完整,避免遗漏或不必要的依赖项。 - 自定义 Hooks:提取可复用的逻辑,提高代码的可维护性和复用性。
- 性能优化:合理使用 Hooks,如
useCallback
和useMemo
,避免不必要的重新渲染和计算。 - 与 Vue 3 的对比:理解 React 和 Vue 3 在生命周期管理上的异同,有助于快速上手 React。
通过本文的深入解析,你应该能够全面掌握 React 的生命周期及 Hooks,并将这些知识应用于实际项目中,构建出高效、可维护的 React 应用。