`useEffect` 详解:副作用的管理与性能优化

186 阅读4分钟

useEffect 详解:副作用的管理与性能优化

什么是副作用?

副作用(Side Effect)是指在函数执行过程中对外部世界的影响,或者说对函数执行结果以外的事物产生影响。副作用通常指那些不直接影响当前函数计算结果,但可能会改变程序状态或外部环境的操作。例如:

  • 发起网络请求
  • 修改 DOM
  • 订阅事件
  • 设置定时器等

在 React 中,副作用是通过 useEffect 这个 Hook 来管理的。useEffect 让我们能够在函数组件中执行这些副作用,同时还能清理它们,避免内存泄漏或不必要的操作。

useEffect 的基本语法

useEffect 接收两个参数:

  1. 第一个参数是执行副作用的函数。
  2. 第二个参数是一个依赖数组(可选)。它用来告诉 React 何时重新执行副作用。
useEffect(() => {
  // 执行副作用的代码
}, [dependencies]);

执行时间

useEffect 会在组件渲染之后执行。因此,它通常用来处理需要访问 DOM 或进行副作用操作(如网络请求)的任务。

  • 第一次渲染时useEffect 会在组件挂载后执行一次。
  • 后续渲染时:每当组件更新,useEffect 会在每次渲染后执行。

严格模式下的执行

React 18 中,如果启用了严格模式(React.StrictMode),useEffect 可能会被调用两次。这种行为只会发生在开发环境中,主要目的是帮助开发者发现副作用中的潜在问题。React 会在初次渲染时运行 useEffect,然后卸载并重新渲染组件,执行副作用。这可以帮助发现副作用未被正确清理的问题。

然而,组件的挂载时执行的副作用只会执行一次。

useEffect 的使用场景

1. 请求数据

useEffect 是处理数据请求的常见地方。你可以在 useEffect 中发起网络请求,获取数据,并通过更新状态来触发组件重新渲染。

useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setData(data);
  };

  fetchData();
}, []);  // 只在组件挂载时执行一次

2. 监听数据变化

你可以通过依赖数组来监听某些状态或 props 的变化。当依赖项发生变化时,useEffect 会重新执行。这样就可以根据数据的变化更新组件状态或执行其他副作用。

useEffect(() => {
  console.log('Data has changed:', data);
}, [data]);  // 当 data 发生变化时执行

3. 订阅事件

useEffect 还可以用来订阅外部事件,如 resizescroll 或 WebSocket 消息等。

useEffect(() => {
  const handleResize = () => {
    console.log('Window resized');
  };

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);  // 只在组件挂载和卸载时执行

useEffect 的清理机制

useEffect 支持清理副作用。清理函数用于移除订阅、清除定时器或清理其他可能导致内存泄漏的资源。返回的清理函数会在组件卸载时执行,或者在依赖项发生变化之前执行。

清理定时器

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Timer tick');
  }, 1000);

  return () => clearInterval(timer);  // 清理定时器
}, []);

清理事件监听器

useEffect(() => {
  const handleClick = () => console.log('Button clicked');
  
  window.addEventListener('click', handleClick);

  return () => {
    window.removeEventListener('click', handleClick);
  };
}, []);

useEffect 的依赖数组

1. 没有依赖数组

如果没有传入第二个参数(依赖数组),useEffect 会在每次渲染后执行。这种做法一般不推荐,因为它会影响性能,特别是在渲染非常频繁的组件中。

useEffect(() => {
  console.log('Component rendered');
});  // 每次渲染都会执行

2. 空依赖数组

传入空数组 ([]) 表示 useEffect 只会在组件挂载时执行一次,这对于需要在组件初始化时执行的副作用非常有用。例如,在组件加载时获取数据或者设置一次性的订阅。

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

3. 依赖项

传入依赖数组时,useEffect 只会在某个依赖项发生变化时执行。这有助于减少不必要的副作用执行,提高性能。

useEffect(() => {
  console.log('Data updated:', data);
}, [data]);  // 只有 data 发生变化时才执行

性能优化

为了提高性能,避免不必要的副作用执行,可以通过精确控制依赖项来减少执行次数。确保依赖数组只包含那些会触发副作用的变量。

总结

  • 副作用useEffect 用于处理副作用,比如数据请求、事件订阅等。
  • 执行时机useEffect 在每次渲染后执行,并且可以通过依赖数组控制执行时机。
  • 清理副作用:返回的清理函数用于清除副作用,避免内存泄漏。
  • 性能优化:合理使用依赖数组,避免每次渲染都执行副作用。

通过合理使用 useEffect,你可以有效地管理副作用,优化组件的性能,并确保资源得以正确清理。