那个下午,我想砸键盘 😤

0 阅读5分钟

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 这类小工具还挺有意思的。额,虽然可能在大佬们眼里已经是常规操作了,哈哈,但作为前端小新,学到一点新东西,挺有意思的。继续搬砖,继续学习!