Skip to content

ReactJS - 组件生命周期

ReactJS - 使用 Hooks 的组件生命周期

Section titled “ReactJS - 使用 Hooks 的组件生命周期”

在 React 中,组件会经历一个生命周期:挂载(mount,被添加到 DOM 中)、更新(update,由于 props 或 state 变化而重新渲染)和卸载(unmount,从 DOM 中移除)。过去,类组件使用特定的生命周期方法(例如 componentDidMount、componentWillUnmount)。在现代 React 中,函数组件主要使用 useEffect hook 来处理与这些生命周期阶段相关的 side effects(副作用)和操作。

useEffect 允许您在函数组件中执行 side effects。常见的 side effects 包括数据获取、订阅或手动更改 DOM。默认情况下,它在每次渲染后运行,但您可以控制它何时运行。

其基本语法是 useEffect(setupFunction, dependenciesArray?):

  • setupFunction:包含 side effect 逻辑的函数。它可以选择返回一个 cleanupFunction(清理函数)。
  • dependenciesArray(可选):一个值数组(props 或 state),effect 依赖于这些值。只有当这些值在两次渲染之间发生变化时,effect 才会重新运行。如果省略,effect 在每次渲染后运行。如果提供一个空数组 [],effect 只在初始渲染(挂载)后运行一次,并且清理函数在卸载时运行。
  • 初始化(类似 constructor):在函数组件中,state 的初始化使用 useState 完成,refs 的初始化使用 useRef 完成。这发生在组件函数首次被调用时。
  • 挂载(类似 componentDidMount):要使 effect 仅在组件挂载后运行一次,请向 useEffect 提供一个空的依赖项数组 []。这非常适合初始数据获取或设置订阅。
  • 更新(类似 componentDidUpdate):要在特定 props 或 state 改变时运行 effect,请将这些值包含在依赖项数组中。effect 将在初始渲染后以及任何依赖项发生变化的后续渲染后运行。
  • 卸载(类似 componentWillUnmount):如果 useEffect 中的 setupFunction 返回另一个函数(即 cleanupFunction),React 将在组件卸载之前,以及在由于依赖项改变而重新运行 effect 之前运行此清理函数。这对于清理订阅或计时器等资源以防止内存泄漏至关重要。
  • componentWillReceiveProps(已弃用):在函数组件中,由于 prop 改变引起的重新渲染是自动处理的。如果您需要响应 prop 改变执行 side effect,请使用 useEffect 并将该 prop 包含在其依赖项数组中。
  • componentWillMount 和 componentWillUpdate(已弃用):这些较旧的、在渲染之前运行的类生命周期方法已被弃用。它们的大多数用例可以通过在渲染之后使用 useEffect 运行 effect,或者直接在组件体内初始化 state 来实现。

与类组件中的 PureComponent 或 shouldComponentUpdate 类似,React.memo 是一个高阶组件(higher-order component),它会记忆(memoize)您的函数组件。如果您的组件在给定相同的 props 时渲染相同的结果,React 可以通过重用上次渲染的结果来跳过重新渲染该组件。这对于那些经常使用相同 props 渲染的组件来说是一个有用的性能优化手段。

让我们改编原始示例。App 是一个父组件,它更新一个数字;Content 是一个子组件,它使用 useEffect 在其生命周期阶段记录消息。

import React, { useState, useEffect } from 'react';
// Child Component
// 子组件
function Content({ myNumber }) {
useEffect(() => {
console.log('Content: Component did mount (or myNumber changed for the first time)');
// This effect runs on mount AND when myNumber changes.
// 这个 effect 在挂载时和 myNumber 改变时都会运行。
// If you only want it on mount, the dependency array would be [].
// 如果您只想让它在挂载时运行,依赖项数组应为 []。
// If you want it specifically for myNumber updates after mount, you might need a ref to track if it's the first render.
// 如果您希望它专门针对挂载后 myNumber 的更新,您可能需要一个 ref 来跟踪是否是首次渲染。
console.log('Content: myNumber is now:', myNumber);
return () => {
// This cleanup runs before the effect runs again (if myNumber changes) OR on unmount.
// 这个清理函数在 effect 再次运行之前(如果 myNumber 改变)或在卸载时运行。
console.log('Content: Cleanup before next myNumber update or unmount. Current myNumber:', myNumber);
};
}, [myNumber]); // Dependency: effect re-runs if myNumber changes
// 依赖项:如果 myNumber 改变,effect 会重新运行
useEffect(() => {
console.log('Content: Effect for MOUNT only');
// effect 只在挂载时运行
return () => {
console.log('Content: Effect for UNMOUNT only - Component will unmount!');
// effect 只在卸载时运行 - 组件即将卸载!
};
}, []); // Empty dependency array: runs only on mount and unmount
// 空依赖项数组:只在挂载和卸载时运行
return (
<div>
<h3>Received Number: {myNumber}</h3>
</div>
);
}
// Parent Component
// 父组件
function App() {
const [data, setData] = useState(0);
const [showContent, setShowContent] = useState(true);
const setNewNumber = () => {
setData(prevData => prevData + 1);
};
// This simulates the unmounting from the original example
// 这模拟了原始示例中的卸载逻辑
useEffect(() => {
if (data > 2) { // For demonstration, unmount after a few increments
// 为了演示,增加几次后卸载
console.log('App: Preparing to unmount Content component in 2 seconds');
// App:准备在 2 秒后卸载 Content 组件
setTimeout(() => {
setShowContent(false);
console.log('App: Content component unmounted.');
// App:Content 组件已卸载。
}, 2000);
}
}, [data]);
return (
<div>
<button onClick={setNewNumber}>INCREMENT</button>
{showContent ? <Content myNumber={data} /> : <p>Content has been unmounted.</p>}
{/* 如果 showContent 为 true 则显示 Content 组件,否则显示文本 */}
</div>
);
}
export default App;
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
const container = document.getElementById('app');
const root = ReactDOM.createRoot(container);
root.render(<App />);
// The unmounting logic is now handled within App.jsx for this example's purpose.
// 卸载逻辑在此示例中已移至 App.jsx 内处理。
// Original example had: setTimeout(() => { ReactDOM.unmountComponentAtNode(document.getElementById('app')); }, 10000);
// 原始示例:setTimeout(() => { ReactDOM.unmountComponentAtNode(document.getElementById('app')); }, 10000);
// With React 18, it would be: setTimeout(() => { root.unmount(); }, 10000);
// 在 React 18 中应为:setTimeout(() => { root.unmount(); }, 10000);
  1. 初始渲染:
    • Content: Component did mount (or myNumber changed for the first time)
    • Content: myNumber is now: 0
    • Content: Effect for MOUNT only
  2. 点击 INCREMENT(data 变为 1):
    • Content: Cleanup before next myNumber update or unmount. Current myNumber: 0
    • Content: Component did mount (or myNumber changed for the first time)(Effect 由于 myNumber 改变而重新运行)
    • Content: myNumber is now: 1
  3. 再次点击 INCREMENT(data 变为 2,然后 3 触发卸载逻辑): myNumber 改变为 2 时有类似的日志。当 data 变为 3 时:
    • App: Preparing to unmount Content component in 2 seconds (2 秒后,Content 卸载)
    • Content: Cleanup before next myNumber update or unmount. Current myNumber: 3
    • Content: Effect for UNMOUNT only - Component will unmount!
    • App: Content component unmounted.

理解 useEffect 的依赖项数组至关重要。它控制着您的 effect 何时运行和清理,防止不必要的操作和 bug。始终确保您的依赖项数组准确地反映了 effect 中使用的所有来自组件作用域的值。