React 中的 useEffect Hook 的清理机制(Cleanup),以及为什么需要清理

160 阅读2分钟

useEffect 的清理机制是 React 中处理副作用的重要部分,它可以防止内存泄漏和避免出现意外行为。

1. 为什么需要清理?

在以下情况下需要清理:

  • 取消订阅(例如 WebSocket 连接)
  • 清除定时器
  • 取消网络请求
  • 移除事件监听器

如果不进行清理,可能会导致:

  • 内存泄漏
  • 组件卸载后仍在执行的副作用
  • 过时的状态更新
  • 重复的事件监听

2. 基本清理示例

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    // 设置定时器
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    // 清理函数
    return () => {
      clearInterval(timer); // 清除定时器
    };
  }, []); // 空依赖数组,只在组件挂载时执行

  return <div>Count: {count}</div>;
}

3. 事件监听器清理

import React, { useState, useEffect } from 'react';

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    // 定义处理函数
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // 添加事件监听器
    window.addEventListener('resize', handleResize);

    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组,只在组件挂载时执行

  return (
    <div>
      Window size: {windowSize.width} x {windowSize.height}
    </div>
  );
}

4. 异步操作的清理

import React, { useState, useEffect } from 'react';

function UserData({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isMounted = true; // 标记组件是否已挂载

    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        
        // 只在组件仍然挂载时更新状态
        if (isMounted) {
          setUser(data);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          console.error('Error fetching user:', error);
          setLoading(false);
        }
      }
    };

    fetchUser();

    // 清理函数
    return () => {
      isMounted = false; // 标记组件已卸载
    };
  }, [userId]); // 依赖于 userId

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>No user data</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

5. WebSocket 连接的清理

import React, { useState, useEffect } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket(`ws://example.com/chat/${roomId}`);

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    // 清理函数
    return () => {
      ws.close(); // 关闭 WebSocket 连接
    };
  }, [roomId]);

  return (
    <div>
      {messages.map((msg, index) => (
        <div key={index}>{msg.text}</div>
      ))}
    </div>
  );
}

6. 订阅模式的清理

import React, { useEffect } from 'react';
import { eventEmitter } from './eventEmitter';

function NotificationListener() {
  useEffect(() => {
    const handleNotification = (message) => {
      console.log('New notification:', message);
    };

    // 订阅事件
    eventEmitter.on('notification', handleNotification);

    // 清理函数
    return () => {
      // 取消订阅
      eventEmitter.off('notification', handleNotification);
    };
  }, []);

  return <div>Listening for notifications...</div>;
}

清理机制的重要注意事项:

  1. 执行时机

    • 组件卸载时
    • 依赖项改变导致效果重新执行时
    • 组件重新渲染时(如果没有依赖项)
  2. 常见错误

useEffect(() => {
  // ❌ 错误:清理函数中使用了过时的闭包值
  const timer = setInterval(() => {
    console.log(count); // 可能使用的是旧的 count 值
  }, 1000);

  return () => clearInterval(timer);
}, []); // 依赖项数组为空

// ✅ 正确:使用函数更新形式
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1); // 使用函数更新形式
  }, 1000);

  return () => clearInterval(timer);
}, []);
  1. 性能优化
useEffect(() => {
  const handleScroll = () => {
    // 处理滚动事件
  };

  // 使用防抖优化滚动事件监听
  const debouncedHandler = debounce(handleScroll, 100);
  window.addEventListener('scroll', debouncedHandler);

  return () => {
    window.removeEventListener('scroll', debouncedHandler);
    // 取消未执行的防抖函数
    debouncedHandler.cancel();
  };
}, []);

正确使用清理机制可以防止内存泄漏和意外行为,使得组件更加健壮和可靠。在编写 useEffect 时,应该始终考虑是否需要清理,以及如何正确实现清理功能。