Skip to content

内存泄漏预防

第一部分:清理模式

1.1 useEffect清理

javascript
// 基本清理模式
function Component() {
  useEffect(() => {
    // 设置
    const resource = setupResource();
    
    // 清理
    return () => {
      cleanupResource(resource);
    };
  }, []);
}

// 定时器清理
function TimerCleanup() {
  useEffect(() => {
    const timerId = setInterval(() => {
      console.log('tick');
    }, 1000);
    
    return () => clearInterval(timerId);
  }, []);
}

// 多个定时器
function MultipleTimers() {
  useEffect(() => {
    const timer1 = setInterval(() => {}, 1000);
    const timer2 = setTimeout(() => {}, 5000);
    const rafId = requestAnimationFrame(() => {});
    
    return () => {
      clearInterval(timer1);
      clearTimeout(timer2);
      cancelAnimationFrame(rafId);
    };
  }, []);
}

// 事件监听器清理
function EventListenerCleanup() {
  useEffect(() => {
    const handleResize = () => console.log('resize');
    const handleScroll = () => console.log('scroll');
    
    window.addEventListener('resize', handleResize);
    window.addEventListener('scroll', handleScroll);
    
    return () => {
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
}

// WebSocket清理
function WebSocketCleanup({ url }) {
  useEffect(() => {
    const ws = new WebSocket(url);
    
    ws.onopen = () => console.log('connected');
    ws.onmessage = (event) => console.log(event.data);
    ws.onerror = (error) => console.error(error);
    
    return () => {
      ws.close();
    };
  }, [url]);
}

// IntersectionObserver清理
function ObserverCleanup() {
  const ref = useRef();
  
  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      console.log('intersection', entries);
    });
    
    if (ref.current) {
      observer.observe(ref.current);
    }
    
    return () => {
      observer.disconnect();
    };
  }, []);
  
  return <div ref={ref}>Content</div>;
}

// MutationObserver清理
function MutationCleanup() {
  useEffect(() => {
    const observer = new MutationObserver((mutations) => {
      console.log('mutations', mutations);
    });
    
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
    
    return () => {
      observer.disconnect();
    };
  }, []);
}

1.2 AbortController模式

javascript
// 取消fetch请求
function FetchWithAbort({ url }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(setData)
      .catch(error => {
        if (error.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          console.error('Fetch error:', error);
        }
      });
    
    return () => {
      controller.abort();
    };
  }, [url]);
  
  return <div>{data}</div>;
}

// 多个请求
function MultipleFetchCleanup() {
  useEffect(() => {
    const controllers = [];
    
    const urls = ['/api/users', '/api/posts', '/api/comments'];
    
    urls.forEach(url => {
      const controller = new AbortController();
      controllers.push(controller);
      
      fetch(url, { signal: controller.signal })
        .then(res => res.json())
        .then(data => console.log(data))
        .catch(err => {
          if (err.name !== 'AbortError') {
            console.error(err);
          }
        });
    });
    
    return () => {
      controllers.forEach(controller => controller.abort());
    };
  }, []);
}

// axios取消
function AxiosCleanup() {
  useEffect(() => {
    const source = axios.CancelToken.source();
    
    axios.get('/api/data', {
      cancelToken: source.token
    })
    .then(response => console.log(response.data))
    .catch(error => {
      if (axios.isCancel(error)) {
        console.log('Request cancelled');
      }
    });
    
    return () => {
      source.cancel('Component unmounted');
    };
  }, []);
}

1.3 自定义Hook清理

javascript
// useEventListener Hook
function useEventListener(eventName, handler, element = window) {
  const savedHandler = useRef(handler);
  
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  
  useEffect(() => {
    const eventListener = (event) => savedHandler.current(event);
    
    element.addEventListener(eventName, eventListener);
    
    return () => {
      element.removeEventListener(eventName, eventListener);
    };
  }, [eventName, element]);
}

// useInterval Hook
function useInterval(callback, delay) {
  const savedCallback = useRef(callback);
  
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  
  useEffect(() => {
    if (delay === null) return;
    
    const tick = () => savedCallback.current();
    const id = setInterval(tick, delay);
    
    return () => clearInterval(id);
  }, [delay]);
}

// useWebSocket Hook
function useWebSocket(url) {
  const [data, setData] = useState(null);
  const wsRef = useRef(null);
  
  useEffect(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;
    
    ws.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    return () => {
      if (wsRef.current) {
        wsRef.current.close();
        wsRef.current = null;
      }
    };
  }, [url]);
  
  return data;
}

// useObserver Hook
function useObserver(callback, options) {
  const elementRef = useRef();
  
  useEffect(() => {
    const observer = new IntersectionObserver(callback, options);
    
    if (elementRef.current) {
      observer.observe(elementRef.current);
    }
    
    return () => {
      observer.disconnect();
    };
  }, [callback, options]);
  
  return elementRef;
}

第二部分:最佳实践

2.1 状态管理

javascript
// ✅ 限制状态大小
function BoundedState() {
  const MAX_ITEMS = 100;
  const [items, setItems] = useState([]);
  
  const addItem = (item) => {
    setItems(prev => {
      const newItems = [...prev, item];
      
      // 限制数组大小
      if (newItems.length > MAX_ITEMS) {
        return newItems.slice(-MAX_ITEMS);
      }
      
      return newItems;
    });
  };
  
  return <button onClick={() => addItem({ id: Date.now() })}>Add</button>;
}

// ✅ 清理过期数据
function CleanupOldData() {
  const [cache, setCache] = useState(new Map());
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCache(prev => {
        const now = Date.now();
        const newCache = new Map();
        
        for (const [key, value] of prev) {
          // 保留1小时内的数据
          if (now - value.timestamp < 3600000) {
            newCache.set(key, value);
          }
        }
        
        return newCache;
      });
    }, 60000);  // 每分钟清理一次
    
    return () => clearInterval(interval);
  }, []);
}

// ✅ 使用WeakMap
function UseWeakMap() {
  // WeakMap的键是弱引用,对象被GC时自动移除
  const cache = useRef(new WeakMap());
  
  const getCachedData = (obj) => {
    if (cache.current.has(obj)) {
      return cache.current.get(obj);
    }
    
    const data = processObject(obj);
    cache.current.set(obj, data);
    return data;
  };
  
  // obj被GC时,cache中的条目也会被移除
}

// ✅ 使用WeakSet
function UseWeakSet() {
  const processedItems = useRef(new WeakSet());
  
  const processItem = (item) => {
    if (processedItems.current.has(item)) {
      return;  // 已处理
    }
    
    doProcess(item);
    processedItems.current.add(item);
  };
}

2.2 引用管理

javascript
// ✅ 及时释放引用
function ReleaseReferences() {
  const heavyDataRef = useRef(null);
  
  useEffect(() => {
    // 加载大对象
    heavyDataRef.current = loadHeavyData();
    
    return () => {
      // 释放引用
      heavyDataRef.current = null;
    };
  }, []);
}

// ✅ 避免循环引用
function AvoidCircularRefs() {
  const objA = { name: 'A' };
  const objB = { name: 'B' };
  
  // ❌ 循环引用
  objA.ref = objB;
  objB.ref = objA;
  
  // ✅ 使用WeakMap打破循环
  const refs = new WeakMap();
  refs.set(objA, objB);
  refs.set(objB, objA);
}

// ✅ 清理DOM引用
function DOMRefCleanup() {
  const elementRef = useRef();
  
  useEffect(() => {
    const element = document.getElementById('my-element');
    elementRef.current = element;
    
    return () => {
      elementRef.current = null;  // 清理引用
    };
  }, []);
}

2.3 缓存策略

javascript
// ✅ LRU缓存(最近最少使用)
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }
  
  get(key) {
    if (!this.cache.has(key)) return undefined;
    
    const value = this.cache.get(key);
    
    // 移到最后(最近使用)
    this.cache.delete(key);
    this.cache.set(key, value);
    
    return value;
  }
  
  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    
    this.cache.set(key, value);
    
    // 超出容量,删除最老的
    if (this.cache.size > this.capacity) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }
}

// React使用
function useLRUCache(capacity) {
  const cache = useRef(new LRUCache(capacity));
  return cache.current;
}

// ✅ 时间限制缓存
class TTLCache {
  constructor(ttl = 60000) {
    this.ttl = ttl;
    this.cache = new Map();
  }
  
  set(key, value) {
    this.cache.set(key, {
      value,
      expiry: Date.now() + this.ttl
    });
  }
  
  get(key) {
    const item = this.cache.get(key);
    
    if (!item) return undefined;
    
    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return undefined;
    }
    
    return item.value;
  }
  
  cleanup() {
    const now = Date.now();
    
    for (const [key, item] of this.cache) {
      if (now > item.expiry) {
        this.cache.delete(key);
      }
    }
  }
}

// 定期清理
function useTTLCache(ttl) {
  const cache = useRef(new TTLCache(ttl));
  
  useEffect(() => {
    const interval = setInterval(() => {
      cache.current.cleanup();
    }, 60000);  // 每分钟清理一次
    
    return () => clearInterval(interval);
  }, []);
  
  return cache.current;
}

第二部分:React最佳实践

2.1 Hook使用规范

javascript
// ✅ 完整的useEffect清理
function ProperCleanup() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    fetchData().then(result => {
      if (isMounted) {
        setData(result);
      }
    });
    
    return () => {
      isMounted = false;
    };
  }, []);
}

// ✅ useCallback稳定引用
function StableCallbacks() {
  const [items, setItems] = useState([]);
  
  const addItem = useCallback((item) => {
    setItems(prev => [...prev, item]);
  }, []);
  
  const removeItem = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);
  
  return { addItem, removeItem };
}

// ✅ useRef避免闭包陷阱
function AvoidClosureTrap({ value }) {
  const valueRef = useRef(value);
  
  useEffect(() => {
    valueRef.current = value;
  });
  
  useEffect(() => {
    const interval = setInterval(() => {
      console.log(valueRef.current);  // 总是最新值
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);  // 空依赖,不会因value变化重新创建
}

// ✅ 条件清理
function ConditionalCleanup({ enabled }) {
  useEffect(() => {
    if (!enabled) return;
    
    const subscription = subscribe();
    
    return () => {
      subscription.unsubscribe();
    };
  }, [enabled]);
}

2.2 组件设计

javascript
// ✅ 分离关注点
function SeparatedConcerns() {
  // 数据获取
  const data = useData();
  
  // UI渲染
  return <Display data={data} />;
}

function useData() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetch('/api/data', { signal: controller.signal })
      .then(res => res.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });
    
    return () => controller.abort();
  }, []);
  
  return data;
}

// ✅ 组件卸载清理
function CleanupOnUnmount() {
  const resourcesRef = useRef([]);
  
  const createResource = () => {
    const resource = initResource();
    resourcesRef.current.push(resource);
    return resource;
  };
  
  useEffect(() => {
    return () => {
      // 批量清理所有资源
      resourcesRef.current.forEach(resource => {
        resource.cleanup();
      });
      resourcesRef.current = [];
    };
  }, []);
  
  return <div>Component</div>;
}

// ✅ 条件渲染清理
function ConditionalRender({ show }) {
  return (
    <div>
      {show && <HeavyComponent />}
    </div>
  );
  // show为false时,HeavyComponent卸载并清理
}

2.3 全局状态管理

javascript
// ✅ Redux清理
function ReduxCleanup() {
  const dispatch = useDispatch();
  
  useEffect(() => {
    // 订阅
    const unsubscribe = store.subscribe(() => {
      console.log('state changed');
    });
    
    return () => {
      unsubscribe();
    };
  }, []);
  
  useEffect(() => {
    return () => {
      // 组件卸载时清理状态
      dispatch({ type: 'CLEANUP' });
    };
  }, [dispatch]);
}

// ✅ Context清理
const DataContext = React.createContext();

function DataProvider({ children }) {
  const [data, setData] = useState({});
  const subscribersRef = useRef(new Set());
  
  const subscribe = useCallback((callback) => {
    subscribersRef.current.add(callback);
    
    return () => {
      subscribersRef.current.delete(callback);
    };
  }, []);
  
  useEffect(() => {
    return () => {
      // 清理所有订阅
      subscribersRef.current.clear();
    };
  }, []);
  
  return (
    <DataContext.Provider value={{ data, subscribe }}>
      {children}
    </DataContext.Provider>
  );
}

// ✅ 状态持久化清理
function PersistentState() {
  const [state, setState] = useState(() => {
    const saved = localStorage.getItem('myState');
    return saved ? JSON.parse(saved) : {};
  });
  
  useEffect(() => {
    localStorage.setItem('myState', JSON.stringify(state));
  }, [state]);
  
  useEffect(() => {
    return () => {
      // 可选:卸载时清理
      localStorage.removeItem('myState');
    };
  }, []);
}

第三部分:预防性编码

3.1 ESLint规则

javascript
// .eslintrc.js
module.exports = {
  extends: ['react-app', 'react-hooks'],
  rules: {
    'react-hooks/exhaustive-deps': 'warn',
    'react-hooks/rules-of-hooks': 'error'
  },
  plugins: ['react-hooks']
};

// 自定义规则检测泄漏
module.exports = {
  rules: {
    'no-uncleaned-effects': {
      meta: {
        type: 'problem',
        docs: {
          description: 'Enforce cleanup in useEffect'
        }
      },
      create(context) {
        return {
          CallExpression(node) {
            if (node.callee.name === 'useEffect') {
              const callback = node.arguments[0];
              
              // 检查是否有return语句
              const hasReturn = callback.body.body.some(
                statement => statement.type === 'ReturnStatement'
              );
              
              if (!hasReturn) {
                context.report({
                  node,
                  message: 'useEffect should return a cleanup function'
                });
              }
            }
          }
        };
      }
    }
  }
};

3.2 TypeScript类型安全

typescript
// 类型化的清理函数
type CleanupFunction = () => void;

function useResource(): CleanupFunction {
  useEffect(() => {
    const resource = createResource();
    
    return () => {
      resource.destroy();
    };
  }, []);
  
  return () => {
    // 额外的清理逻辑
  };
}

// 强制清理
interface SubscriptionOptions {
  onData: (data: any) => void;
  cleanup: CleanupFunction;  // 必须提供cleanup
}

function useSubscription(options: SubscriptionOptions) {
  useEffect(() => {
    const subscription = subscribe(options.onData);
    
    return () => {
      subscription.unsubscribe();
      options.cleanup();  // 强制调用
    };
  }, [options]);
}

3.3 测试覆盖

javascript
// 测试清理逻辑
describe('Component cleanup', () => {
  it('should cleanup timer on unmount', () => {
    jest.useFakeTimers();
    
    const { unmount } = render(<TimerComponent />);
    
    // 验证定时器启动
    expect(setInterval).toHaveBeenCalled();
    
    unmount();
    
    // 验证定时器清理
    expect(clearInterval).toHaveBeenCalled();
    
    jest.useRealTimers();
  });
  
  it('should cleanup event listener on unmount', () => {
    const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
    const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
    
    const { unmount } = render(<ListenerComponent />);
    
    expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
    
    unmount();
    
    expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
  });
});

// E2E内存测试
// test/memory.spec.js
const puppeteer = require('puppeteer');

describe('Memory leak tests', () => {
  let browser, page;
  
  beforeAll(async () => {
    browser = await puppeteer.launch({ args: ['--expose-gc'] });
  });
  
  beforeEach(async () => {
    page = await browser.newPage();
  });
  
  afterEach(async () => {
    await page.close();
  });
  
  afterAll(async () => {
    await browser.close();
  });
  
  it('should not leak memory on modal open/close', async () => {
    await page.goto('http://localhost:3000');
    
    const getMemory = () => page.evaluate(() => performance.memory.usedJSHeapSize);
    
    const initial = await getMemory();
    
    // 打开关闭模态框50次
    for (let i = 0; i < 50; i++) {
      await page.click('#open-modal');
      await page.waitForSelector('.modal');
      await page.click('#close-modal');
      await page.waitForFunction(() => !document.querySelector('.modal'));
    }
    
    // 强制GC
    await page.evaluate(() => {
      if (window.gc) window.gc();
    });
    
    const final = await getMemory();
    const growth = ((final - initial) / initial) * 100;
    
    console.log('Memory growth:', growth.toFixed(2), '%');
    
    // 允许20%的增长
    expect(growth).toBeLessThan(20);
  });
});

注意事项

1. 清理的完整性

javascript
// ✅ 完整的清理检查清单
function CompleteCleanup() {
  useEffect(() => {
    // 1. 定时器
    const timer = setInterval(() => {}, 1000);
    
    // 2. 事件监听
    const handler = () => {};
    window.addEventListener('resize', handler);
    
    // 3. 订阅
    const subscription = subscribe();
    
    // 4. WebSocket
    const ws = new WebSocket('ws://...');
    
    // 5. Observer
    const observer = new IntersectionObserver(() => {});
    
    // 6. 请求
    const controller = new AbortController();
    
    return () => {
      clearInterval(timer);
      window.removeEventListener('resize', handler);
      subscription.unsubscribe();
      ws.close();
      observer.disconnect();
      controller.abort();
    };
  }, []);
}

2. 开发环境检查

javascript
// 严格模式检测
// StrictMode会双重调用effects
<React.StrictMode>
  <App />
</React.StrictMode>

// 如果清理不当,会看到明显问题

3. 生产监控

javascript
// 监控内存使用
function useMemoryMonitoring() {
  useEffect(() => {
    const interval = setInterval(() => {
      if (performance.memory) {
        const used = performance.memory.usedJSHeapSize;
        const limit = performance.memory.jsHeapSizeLimit;
        const percent = (used / limit) * 100;
        
        if (percent > 90) {
          // 告警
          console.error('Memory usage critical:', percent.toFixed(2), '%');
          sendAlert('high-memory-usage', { percent });
        }
      }
    }, 30000);  // 每30秒检查
    
    return () => clearInterval(interval);
  }, []);
}

常见问题

Q1: 如何确认清理函数被调用?

A: 添加console.log或使用React DevTools。

Q2: 清理函数可以是异步的吗?

A: 不可以,必须是同步函数。

Q3: WeakMap和Map的区别?

A: WeakMap的键是弱引用,对象被GC时自动清理。

Q4: 如何预防第三方库泄漏?

A: 阅读文档,确保正确调用销毁方法。

Q5: 状态过大会泄漏吗?

A: 不算泄漏,但应限制大小避免内存压力。

Q6: 开发环境比生产环境更容易泄漏吗?

A: StrictMode会暴露问题,但泄漏本身无差异。

Q7: 如何测试清理逻辑?

A: 编写unmount测试,验证清理函数调用。

Q8: 闭包一定会泄漏吗?

A: 不一定,但要注意闭包引用的对象大小。

Q9: HOC和Hook哪个更容易泄漏?

A: 都可能泄漏,关键是正确的清理逻辑。

Q10: React 19对泄漏预防有帮助吗?

A: 更好的开发工具和严格检查有助于预防。

总结

核心要点

1. 清理原则
   ✅ 有setup就有cleanup
   ✅ useEffect返回清理函数
   ✅ 组件卸载清理资源
   ✅ 及时释放引用

2. 常见场景
   ✅ 定时器
   ✅ 事件监听
   ✅ 订阅
   ✅ 网络请求

3. 预防措施
   ✅ ESLint规则
   ✅ TypeScript类型
   ✅ 代码审查
   ✅ 自动化测试

最佳实践

1. 开发习惯
   ✅ 编写cleanup同时编写setup
   ✅ 使用自定义Hook封装
   ✅ 注意闭包引用
   ✅ 限制缓存大小

2. 测试策略
   ✅ 单元测试清理逻辑
   ✅ E2E测试内存增长
   ✅ 定期手动检查
   ✅ 生产监控

3. 团队规范
   ✅ 代码审查重点检查
   ✅ 文档清理要求
   ✅ 分享最佳实践
   ✅ 持续学习改进

预防胜于治疗,建立良好的编码习惯和规范是避免内存泄漏的最有效方法。