【React】useEffect、useLayoutEffect 和 useInsertionEffect 注意点

张开发
2026/4/6 6:44:45 15 分钟阅读

分享文章

【React】useEffect、useLayoutEffect 和 useInsertionEffect 注意点
useEffect依赖项的作用以及性能优化点明核心作用useEffect的依赖项数组其核心目的是精确控制副作用函数的执行时机优化性能避免不必要的计算或 API 调用确保effect内部能访问到最新的props和state分类讨论不提供依赖项数effect会在每次组件渲染包括首次渲染和所有更新后执行提供一个空数组[]effect只会在组件首次挂载(mount)后执行一次其返回的清理函数会在组件卸载(unmount)时执行一次提供一个包含依赖项的数组[dep1,dep2]首次挂载后执行后续的每一次渲染后React会使用Object.is()比较数组中每一个依赖项的当前值和上一次渲染时的值。比较机制原始类型比较的是值引用类型比较的是引用地址优化优化手段依赖项使用 useCallback 和 useMeno 缓存dispatch 函数引用永久稳定可以安全放入依赖项数组先看下以下代码functionApp(){const[query,setQuery]useState(421312)const[results,setResults]useState([])constfetchResults(query){console.log(fetch for:,query)}useEffect((){if(query){fetchResults(query)}},[query,fetchResults]);returninput typetextvalue{query}onChange{esetQuery(e.target.value)}/}这样组件每次重新渲染都会创建一个 fetchResults 函数所以都会重新执行 useEffect造成性能浪费。所以我们可以使用 useCallback 来优化。constfetchResultsuseCallback((query){console.log(fetch for:,query)},[userId])这样只有在 userId 改变时才会重新渲染函数。当然还有一种做法就是将函数写在 useEffect 内部也能解决这个问题useEffect((){constfetchResults(query){console.log(fetch for:,query)}if(query){fetchResults(query)}},[query]);还有就是父子组件传参传递对象的形式这样可以将对象在子组件中解构然后只关注单个属性的值。还有一种做法是将子组件用 memo 包裹这样的话父组件的 props 不改变那么子组件就不会重新渲染。异步处理和竞态条件在 React 的useEffect中避免竞态条件Race Condition主要是防止异步操作比如 API 请求在组件卸载后或依赖更新后依然尝试更新已卸载或已更新的组件状态。导致UI 显示过时的数据或者导致警告或者内存泄露。方法一使用一个标志位 / 状态锁推荐import { useEffect, useState } from react; function MyComponent() { const [data, setData] useState(null); useEffect(() { let isCancelled false; async function fetchData() { const response await fetch(https://api.example.com/data); const result await response.json(); if (!isCancelled) { setData(result); // 只有组件还挂载时才更新状态 } } fetchData(); return () { isCancelled true; // 组件卸载或依赖变化设置取消标志 }; }, []); // 添加依赖 return div{data ? JSON.stringify(data) : Loading...}/div; }方法二使用AbortController等中断请求useEffect(() { const controller new AbortController(); async function fetchData() { try { const response await fetch(https://api.example.com/data, { signal: controller.signal, }); const result await response.json(); setData(result); } catch (error) { if (error.name AbortError) { console.log(请求被取消); } else { console.error(error); } } } fetchData(); return () { controller.abort(); // 强制中断请求 }; }, []);如果像是 websocket 这样的我们可以在 协议层中断请求比如前后端约定一个 type 为 interrupt 那么另一段主要断开连接。方法三引入唯一标识适用于多次请求一般由后端提供useEffect(() { let currentRequestId Date.now(); // 当前请求唯一标识 async function fetchData() { const response await fetch(https://api.example.com/data); const result await response.json(); if (currentRequestId latestRequestId) { setData(result); } } const latestRequestId currentRequestId; fetchData(); }, [someDependency]);方法适用场景标志位法isCancelled任何异步操作AbortController只适用于fetch请求请求 ID 标记法短时间内可能多次请求useLayoutEffectuseLayoutEffect 和 useEffect 有什么区别呢useEffect 的 cb是异步调用的会等主线程任务执行完成DOM 更新JS执行完成视图绘制完成才执行。useLayoutEffect 的 cb是同步执行的执行时机是DOM 更新之后视图绘制完成之前这个时间可以更方便的修改 DOM。如果要改 DOM 用useLayoutEffect 其他都用useEffect 。所以 useEffect 的使用场景更加普遍主要用于获取数据、事件监听、订阅、非严格绘制前 DOM 操作主要使用它的异步执行机制不阻塞浏览器渲染。而 useLayoutEffect 主要用于读取 DOM 布局信息并同步更新 DOM如动态计算 tooltip 位置同步执行会阻塞浏览器绘制用于 DOM 修改后绘制改变前。可以避免“页面闪烁”但是耗时可能会导致卡顿而且滥用会引发性能问题。useEffect 导致的闪烁用户可能在两个绘制意见看到一个未调整的 “中间状态”。一些应用场景动态图表尺寸如果对图表闪烁非常敏感的话比如有错位和闪烁使用 useLayoutEffect否则可以使用 useEffect滚动高亮元素因为滚动事件本身就是异步的所以直接可以使用useEffectuseInsertionEffectuseInsertionEffect 比 useLayoutEffect 更早。useInsertionEffect 执行时DOM还没有更新。本质上 useInsertionEffect 主要是解决 css-in-js 在渲染中注入样式的性能问题。哪个和 componentDidMount、componentDidUpdate 更类似componentDidMount、componentDidUpdate 是同步的所以 useLayoutEffect 更类似。

更多文章