Skip to content
第 8 章 前端 ⏱ 12 分钟阅读

第 8 章:生命周期 ​

学习目标 ​

  • 理解函数组件的"生命周期"就是 hooks 调用时机
  • 掌握 useEffect 的三种使用模式
  • 理解 mount / update / unmount 阶段
  • 避开生命周期的 5 个常见坑

一、函数组件的"生命周期" ​

React 函数组件没有传统 componentDidMount,但每个组件实例经历:

阶段含义触发时机
Mount挂载组件首次渲染到 DOM
Update更新props 或 state 变化导致重渲染
Unmount卸载组件从 DOM 移除

每个阶段都有对应的 hook:

tsx
function Comp() {
  // mount 时执行(每次渲染都跑,默认行为)
  useEffect(() => {
    console.log('mount 或 update');

    return () => {
      console.log('清理:update 前/unmount 时');
    };
  });

  // 只在 mount 时执行一次
  useEffect(() => {
    console.log('只在 mount 时');
  }, []);

  return <div />;
}

二、useEffect 三种模式 ​

模式 1:每次渲染都执行 ​

tsx
useEffect(() => {
  console.log('每次渲染');
});     // ⚠️ 不传依赖数组

模式 2:依赖变化时执行 ​

tsx
useEffect(() => {
  console.log('count 变了:', count);
}, [count]);     // ⚠️ 传依赖数组

模式 3:只执行一次(mount) ​

tsx
useEffect(() => {
  console.log('只 mount 一次');
}, []);          // ⚠️ 空数组

三种模式对照 ​

tsx
function App() {
  const [count, setCount] = useState(0);

  // ① 无依赖 → 每次 render 都跑
  useEffect(() => {
    console.log('每次渲染');
  });

  // ② 有依赖 → 依赖变了才跑
  useEffect(() => {
    console.log('count 变了:', count);
  }, [count]);

  // ③ 空依赖 → 只 mount 一次,后续 re-render 不再跑
  useEffect(() => {
    console.log('只跑一次');
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
操作① 无依赖② [count]③ []
首次渲染输出输出输出
点 1 次(count 变 1)输出输出—
点 2 次(count 变 2)输出输出—
点 100 次输出 100 次输出 100 次永远只输出 1 次

[] 的语义:只在 mount 时跑一次,后续 re-render 不再触发——不依赖任何 state/props,所以 React 没理由再执行它。

[] 的典型用法 ​

tsx
useEffect(() => {
  // ① 初始化(只搞一次)
  initChart();
  loadConfig();

  // ② 订阅(WebSocket / EventBus / 全局事件)
  const ws = new WebSocket('wss://...');
  ws.onmessage = handleMsg;

  // ③ 清理(组件卸载时跑)
  return () => ws.close();  // 防内存泄漏
}, []);

⚠️ 坑 4:[] 里别读 state——会拿到 mount 时的旧值,后续变化它看不见:

tsx
const [count, setCount] = useState(0);

useEffect(() => {
  console.log(count); // ❌ 永远是 0(mount 时的快照)
}, []);

解法(二选一):

tsx
// ① 加依赖(count 变了再跑)
useEffect(() => {
  console.log(count);
}, [count]);

// ② 用 ref 同步最新值
const countRef = useRef(count);
countRef.current = count; // 每次 render 同步

useEffect(() => {
  console.log(countRef.current); // ✅ 总是最新
}, []);

💡 [] = 只跑一次 + 卸载清理——挂载初始化、订阅事件、定时器。别在里面读 state(闭包陷阱)。

三、清理函数(Cleanup) ​

useEffect 可以返回一个函数,React 在重新执行 effect 前或组件卸载时调用它:

tsx
useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000);

  return () => {
    clearInterval(id);     // ⚠️ 必须清理,否则内存泄漏
  };
}, []);

常见需要清理的资源:

  • setInterval / setTimeout
  • 事件监听器(addEventListener)
  • WebSocket 连接
  • 订阅(Redux/Zustand)

⚠️ 坑 1:用了 setInterval 不 cleanup,组件卸载后定时器仍在跑,内存泄漏 + 报错"Can't perform state update on unmounted"。

四、生命周期对应关系 ​

类组件函数组件
componentDidMountuseEffect(fn, [])
componentDidUpdateuseEffect(fn, [deps])
componentWillUnmountuseEffect(() => () => cleanup, [])
shouldComponentUpdateReact.memo / useMemo
getDerivedStateFromPropsuseState + useEffect

五、useLayoutEffect(同步版) ​

与 useEffect 类似,但在 DOM 更新后、浏览器绘制前同步执行:

tsx
useLayoutEffect(() => {
  // 同步读 DOM 布局(如读元素宽度然后设置)
  const w = ref.current?.offsetWidth;
  setWidth(w);
}, []);

99% 场景用 useEffect,只有需要同步读 DOM 布局时才用 useLayoutEffect。

六、Strict Mode 双调用 ​

开发模式下 <StrictMode> 会故意mount → unmount → mount 跑一遍,验证 cleanup 是否正确:

tsx
useEffect(() => {
  console.log('A:订阅');
  return () => console.log('A:取消订阅');
}, []);

输出:A:订阅 → A:取消订阅 → A:订阅(故意触发,验证 cleanup 工作正常)

⚠️ 坑 2:不要在 useEffect 里调只执行一次的初始化(比如埋点上报),否则 Strict Mode 会双触发。生产无影响。

七、常见场景代码 ​

监听窗口尺寸 ​

tsx
function useWindowSize() {
  const [size, setSize] = useState({ w: window.innerWidth, h: window.innerHeight });

  useEffect(() => {
    function update() {
      setSize({ w: window.innerWidth, h: window.innerHeight });
    }
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  return size;
}

拉数据 ​

tsx
function UserList() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    fetch('/api/users')
      .then(r => r.json())
      .then(setUsers);
  }, []);   // ⚠️ 空依赖,只拉一次
}

⚠️ 坑 3:依赖数组漏写依赖会导致 effect 用到旧值。用 ESLint 插件 exhaustive-deps 帮你检查。

八、本章小结 ​

要点关键
三种模式不传 / [dep] / []
Cleanup返回函数,清理定时器/订阅
mountuseEffect(fn, [])
unmountcleanup 在 unmount 时触发
Strict Mode开发模式故意双 mount 验证 cleanup
useLayoutEffect极少数需要同步 DOM 的场景
数据拉取mount 时一次,加 cleanup 防泄漏

动手练习 ​

  1. 计时器:做一个秒表组件,开始/停止按钮,停止时清理定时器
  2. 窗口尺寸:写一个 useWindowSize hook(见上),在组件里展示宽高
  3. 拉数据:写一个组件,挂载时 fetch 一次用户列表,展示 loading/数据/错误三态
  4. 找坑:以下代码有什么问题?
    tsx
    useEffect(() => {
      const id = setInterval(() => console.log(count), 1000);
    }, []);

下一章:第 9 章:Hooks 基础 →

本站基于 VitePress 构建 · 由 StackHub 团队维护