
useEffect 完全指南一、useEffect 基础1.1 基本语法useEffect(effect, dependencies?)effect副作用函数可以返回清理函数dependencies依赖数组控制 effect 执行时机1.2 执行时机function EffectTiming() { const [count, setCount] useState(0); // 1. 每次渲染后都执行 useEffect(() { console.log(每次渲染后执行); }); // 2. 仅在挂载时执行一次 useEffect(() { console.log(只在挂载时执行); }, []); // 3. 依赖变化时执行 useEffect(() { console.log(count 变化时执行); }, [count]); return button onClick{() setCount(count 1)}点击/button; }二、依赖数组详解2.1 依赖数组的类型依赖数组执行时机适用场景无依赖每次渲染后同步 DOM、日志记录[]空数组仅挂载时API 请求、订阅、设置定时器[dep1, dep2]依赖变化时响应特定状态变化2.2 依赖项的选择function DependencyExample({ userId, onComplete }) { const [data, setData] useState(null); useEffect(() { // ✅ 使用所有响应式值 const fetchData async () { const result await fetch(/api/users/${userId}); setData(await result.json()); onComplete?.(); }; fetchData(); }, [userId, onComplete]); // 包含所有外部依赖 // ❌ 缺少依赖可能导致过期数据 useEffect(() { console.log(data); // 缺少 data 依赖 }, []); // eslint 会警告 }2.3 使用 ESLint 规则// .eslintrc.json{rules:{react-hooks/exhaustive-deps:error}}三、清理函数Cleanup3.1 清理订阅function EventListener() { const [position, setPosition] useState({ x: 0, y: 0 }); useEffect(() { const handleMove (e) { setPosition({ x: e.clientX, y: e.clientY }); }; window.addEventListener(mousemove, handleMove); // 清理移除事件监听 return () { window.removeEventListener(mousemove, handleMove); }; }, []); return div鼠标位置: {position.x}, {position.y}/div; }3.2 清理定时器function Timer() { const [seconds, setSeconds] useState(0); useEffect(() { const interval setInterval(() { setSeconds(prev prev 1); }, 1000); // 清理清除定时器 return () clearInterval(interval); }, []); return div计时: {seconds} 秒/div; }3.3 取消网络请求function DataFetcher({ url }) { const [data, setData] useState(null); const [loading, setLoading] useState(true); useEffect(() { let isMounted true; const abortController new AbortController(); const fetchData async () { try { const response await fetch(url, { signal: abortController.signal }); const result await response.json(); if (isMounted) { setData(result); setLoading(false); } } catch (error) { if (isMounted error.name ! AbortError) { console.error(error); } } }; fetchData(); // 清理取消请求 return () { isMounted false; abortController.abort(); }; }, [url]); if (loading) return div加载中.../div; return div{JSON.stringify(data)}/div; }3.4 清理防抖/节流function SearchInput() { const [searchTerm, setSearchTerm] useState(); const [results, setResults] useState([]); useEffect(() { if (!searchTerm) return; const timer setTimeout(async () { const response await fetch(/api/search?q${searchTerm}); const data await response.json(); setResults(data); }, 500); // 清理清除未执行的定时器 return () clearTimeout(timer); }, [searchTerm]); return ( div input onChange{(e) setSearchTerm(e.target.value)} / ul{results.map(item li key{item.id}{item.name}/li)}/ul /div ); }四、常见使用场景4.1 页面标题更新function PageTitle({ title }) { useEffect(() { const originalTitle document.title; document.title title; return () { document.title originalTitle; }; }, [title]); return null; }4.2 滚动位置恢复function ScrollRestoration() { const [scrollPosition, setScrollPosition] useState(0); useEffect(() { const handleScroll () { setScrollPosition(window.scrollY); }; window.addEventListener(scroll, handleScroll); return () window.removeEventListener(scroll, handleScroll); }, []); const scrollToTop () { window.scrollTo({ top: 0, behavior: smooth }); }; return ( div button onClick{scrollToTop}回到顶部/button p滚动位置: {scrollPosition}px/p /div ); }4.3 本地存储同步function useLocalStorage(key, initialValue) { const [value, setValue] useState(() { const saved localStorage.getItem(key); return saved ? JSON.parse(saved) : initialValue; }); useEffect(() { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue]; } // 使用 function Settings() { const [theme, setTheme] useLocalStorage(theme, light); return ( select value{theme} onChange{(e) setTheme(e.target.value)} option valuelight亮色/option option valuedark暗色/option /select ); }4.4 WebSocket 连接function WebSocketComponent({ url }) { const [messages, setMessages] useState([]); const [isConnected, setIsConnected] useState(false); useEffect(() { const ws new WebSocket(url); ws.onopen () { setIsConnected(true); console.log(WebSocket 已连接); }; ws.onmessage (event) { setMessages(prev [...prev, event.data]); }; ws.onerror (error) { console.error(WebSocket 错误:, error); }; ws.onclose () { setIsConnected(false); console.log(WebSocket 已断开); }; return () { ws.close(); }; }, [url]); return ( div div状态: {isConnected ? 已连接 : 未连接}/div ul{messages.map((msg, i) li key{i}{msg}/li)}/ul /div ); }五、性能优化5.1 跳过不必要的 effectfunction OptimizedEffect({ data }) { const [processed, setProcessed] useState(null); useEffect(() { // 只有当 data 真正变化时才处理 const result expensiveComputation(data); setProcessed(result); }, [data]); // 正确的依赖 // 使用条件跳过 useEffect(() { if (!data?.id) return; // 条件执行 fetchData(data.id); }, [data?.id]); }5.2 使用 useRef 避免重新执行function EffectWithRef() { const isFirstRender useRef(true); useEffect(() { if (isFirstRender.current) { isFirstRender.current false; // 仅在首次渲染时执行的代码 initializeApp(); } }, []); }5.3 拆分多个 effect// ✅ 按职责拆分 function UserProfile({ userId }) { // effect 1: 获取用户数据 useEffect(() { fetchUser(userId).then(setUser); }, [userId]); // effect 2: 更新页面标题 useEffect(() { document.title user?.name || 用户资料; }, [user]); // effect 3: 订阅用户状态 useEffect(() { const subscription subscribeToUserStatus(userId); return () subscription.unsubscribe(); }, [userId]); }六、常见陷阱与解决方案6.1 无限循环// ❌ 导致无限循环 function InfiniteLoop() { const [count, setCount] useState(0); useEffect(() { setCount(count 1); // 每次渲染都更新状态 }); // 无依赖数组 // ✅ 解决方案添加正确的依赖或使用函数式更新 useEffect(() { setCount(prev prev 1); }, []); // 只在挂载时执行一次 }6.2 对象/数组依赖导致频繁执行function BadDependency({ config }) { useEffect(() { // config 每次都是新对象导致 effect 频繁执行 doSomething(config); }, [config]); } // ✅ 解决方案 1使用 useMemo 稳定引用 function GoodDependency({ config }) { const stableConfig useMemo(() config, [config.id, config.name]); useEffect(() { doSomething(stableConfig); }, [stableConfig]); } // ✅ 解决方案 2使用 useRef 跳过 function SkipEffect() { const [count, setCount] useState(0); const countRef useRef(count); useEffect(() { countRef.current count; }); useEffect(() { const timer setInterval(() { console.log(countRef.current); // 始终是最新值 }, 1000); return () clearInterval(timer); }, []); }6.3 useEffect 中的异步函数// ❌ 不能直接传 async 函数 useEffect(async () { const data await fetchData(); setData(data); }, []); // ✅ 在内部定义异步函数 useEffect(() { const fetchDataAsync async () { const data await fetchData(); setData(data); }; fetchDataAsync(); }, []);七、useEffect vs useLayoutEffect特性useEffectuseLayoutEffect执行时机浏览器绘制后DOM 更新后、绘制前阻塞绘制否是适用场景大部分副作用测量 DOM、同步更新function LayoutEffectExample() { const [width, setWidth] useState(0); const divRef useRef(null); // 需要同步读取 DOM 尺寸 useLayoutEffect(() { if (divRef.current) { setWidth(divRef.current.offsetWidth); } }, []); // 避免闪烁 // 非阻塞操作使用 useEffect useEffect(() { console.log(组件已渲染); }, []); return div ref{divRef}宽度: {width}px/div; }八、练习题基础题实现一个组件在挂载时 console.log(‘mounted’)卸载时 console.log(‘unmounted’)实现一个实时时钟每秒更新当前时间进阶题实现一个 Intersection Observer检测元素是否可见实现一个数据轮询组件每 5 秒刷新数据参考答案// 1. 实时时钟 function RealTimeClock() { const [time, setTime] useState(new Date()); useEffect(() { const timer setInterval(() { setTime(new Date()); }, 1000); return () clearInterval(timer); }, []); return div{time.toLocaleTimeString()}/div; } // 2. 可见性检测 function VisibilityDetector() { const [isVisible, setIsVisible] useState(false); const ref useRef(null); useEffect(() { const observer new IntersectionObserver( ([entry]) setIsVisible(entry.isIntersecting), { threshold: 0.5 } ); if (ref.current) observer.observe(ref.current); return () observer.disconnect(); }, []); return ( div ref{ref} style{{ height: 200px, background: #f0f0f0 }} {isVisible ? 元素可见 : 元素不可见} /div ); }九、小结要点说明依赖数组正确声明所有响应式依赖清理函数防止内存泄漏异步处理在 effect 内部定义 async 函数性能优化拆分 effect避免不必要执行执行时机理解 useEffect 和 useLayoutEffect 区别核心要点useEffect 是处理副作用的主要工具始终正确声明依赖数组需要清理的副作用必须返回清理函数使用 ESLint 规则避免遗漏依赖