1. 前言:那个下午,我真的想砸键盘 (〃` 3′〃)
昨天下午写代码,禅道上提了一个bug,“你这个文件列表怎么这么卡?我稍微动下鼠标,页面在抖,输入框会出现失焦问题”。
我当时心想,不应该呀,就一个正常列表页,联调时也没有遇到过呀。立即重新看了下代码逻辑,“明明逻辑没问题,接口也正常,也没死循环,怎么会卡呢”。打开谷歌的性能面板看下,哦。fps掉到30以下,说明Js执行太慢,再点开React DevTools 的"Highlight ,组件在疯狂重渲染。
排查最难受了,尤其是感觉代码逻辑没问题的时候😂,不知道大家咋样,我有时候就是看不出来,挺让人难受的,因为有时间,又不想问ai,最后再看下接口,我排查意识到,哦,列表数据太多了,又因为是定时器轮询,后端每2s推一次列表数据。每次都是一个新的全新的数组对象(引用地址变了),虽然里面内容和上一秒一样。。但 React 的 useState是个“直男”,只看引用不看内容:“哦,新对象?那必须重绘!”
结果就是:CPU 疯狂空转,页面卡顿,用户输入框频频失焦。因为后续还有开发类似的页面。手写了个hooks,一个 useOptimizedState,优化项目还挺好用的。
2. 到底啥是“假更新”?举个栗子 (~ ̄▽ ̄)~
你在做一个即时通讯的消息列表。
-
正常情况:朋友发来新消息,列表多了一行。这时候界面必须刷新,让你看到新消息。✅
-
“假更新”情况:为了保持在线状态,客户端每 3 秒向服务器请求一次“当前消息列表”。服务器说:“没啥变化,还是刚才那 10 条。”
- 如果用普通的
useState:每次收到响应(哪怕内容一模一样,只是内存地址变了),React 都会觉得“哇!新数据!”,然后命令浏览器:“重绘整个列表!” - 后果:虽然你肉眼看不出列表在变,但 CPU 在疯狂空转,风扇呼呼吹,手机发烫,当你想打字时,因为组件正在重绘,输入框可能会丢失焦点或者反应迟钝,无效渲染。
- 如果用普通的
而 useOptimizedState 的作用就是像个守门员:
“哎哎哎,别急!让我看看新数据和旧数据是不是真的一样?哦,都是那 10 条消息?那没事了,该干嘛干嘛去,不用重绘。” 🛑
3. 手把手教你写:代码其实很简单 ( ̄_, ̄ )
代码学习后也就十几行,主要借用了深比较工具lodash.isequal进行比较新旧对象:
import { useState, useCallback } from 'react';
import { isEqual } from 'lodash-es'; // 记得 npm install lodash-es
export function useOptimizedState(initialState) {
const [state, setState] = useState(initialState);
const optimizedSetState = useCallback((newValue) => {
setState((prevState) => {
// 1. 算出真正的新值(支持函数式更新 prev => ...)
const nextState = typeof newValue === 'function'
? newValue(prevState)
: newValue;
// 2. 【灵魂一步】深度比对
// 如果内容完全一样 -> 返回旧状态(React 发现引用没变,跳过渲染)
// 如果内容变了 -> 返回新状态(触发渲染)
return isEqual(prevState, nextState) ? prevState : nextState;
});
}, []);
return [state, optimizedSetState];
}
核心就是那个 isEqual(prevState, nextState)。
- 相等?返回
prevState(旧引用)。React 一看:“哦,还是原来那个对象”,直接罢工不渲染。 - 不等?返回
nextState(新引用)。React 乖乖干活。
4. 实战场景:这玩意儿到底救了我的命 🚑(哈哈,开个玩笑)
光说不练假把式。我在项目里真实遇到的三个场景,用了它之后,效果还是挺好的。
场景一:轮询接口导致的“输入框失焦”bug ψ(`∇´)ψ
痛点:做个后台管理页,有个搜索框。同时有个定时器每 2 秒拉一次列表数据。 以前每次拉回来数据,哪怕列表没变,整个组件树也会重刷。结果就是:用户在搜索框打字,打着打着光标突然没了(失焦)! 用户体验极差
✅ 解决后:
// 替换这一行,
const [list,setList] = useOptimizedState([])l
useEffect(()=>{
const timer = setInterval(async () =>{
const res.data =await =fetchList();
setList(res.data);//即使data是新数组,只要数据不变,绝不重渲染
},2000);
},[])
效果:用户随便打字,光标不闪烁,页面丝滑流畅。
场景二:复杂表单的“局部更新”陷阱 📝
痛点:一个巨大的配置表单,几十个字段的嵌套对象。改个“主题颜色”,结果整个表单重绘,输入延迟明显。
✅ 解决后:
//一样道理
const [config,setConfig] =useOptimizedState(hugeConfig)
//就算手滑输入传入了和之前一样的值,也能拦截
setConfig({...config,theme:"dark"})
场景三:搭配 React.memo 组成“王炸” 💣
这是进阶用法!
- 子组件用了
React.memo(浅比较)。 - 父组件传下来的 props 是个对象。
- 以前:父组件一渲染,props 引用就变,子组件 memo 失效,照样重绘(例如点击按钮,count+1)。
- 现在:父组件用
useOptimizedState稳住 props 引用。只要内容没变,props 引用就不变,子组件彻底不渲染!
5. 避坑指南:别拿着锤子到处敲!`(>﹏<)′
不过这个东西不是万能的,如果是几千行表格数据,isEqual递归遍历时间太久了。可能比react渲染还慢。直接上虚拟滚动就行了。
7. 我就是那个传说中的“结语”(>人<;)
发现 useOptimizedState 这类小工具还挺有意思的。额,虽然可能在大佬们眼里已经是常规操作了,哈哈,但作为前端小新,学到一点新东西,挺有意思的。继续搬砖,继续学习!