React 的“时光胶囊”:useRef 才是那个打破“闭包陷阱”的救世主

前言:它不仅仅是 document.getElementById

如果去面试 React 开发岗位,问到 useRef 是干嘛的,90% 的候选人会说:"用来获取 DOM 元素,比如给 input 设置焦点。"

这就好比你买了一台最新的 iPhone 15 Pro Max,结果只用来打电话。

在 React 的函数式组件(Functional Component)世界里,useRef 其实是一个法外之地 。 它是你在严格的"不可变数据流"和"频繁重渲染"中,唯一的逃生舱(Escape Hatch)

今天咱们不聊怎么 input.focus(),咱们来聊聊怎么用 useRef 搞定那些 useStateuseEffect 搞不定的烂摊子。


核心概念:它是一个"静音"的盒子

首先,你得把 useRef 理解成一个盒子。

  • useState :是大喇叭。你改了里面的值,React 立马大喊:"数据变了!所有组件起立,重新渲染!"
  • useRef :是静音抽屉。你偷偷把里面的值改了,React 根本不知道,组件该干嘛干嘛,不会触发重渲染。

而且,最最重要的是:组件每次重渲染,这个盒子都是同一个盒子(内存地址不变)。

这就赋予了它两个神级能力:"穿越时空""暗度陈仓"


骚操作一:破解"闭包陷阱" (Stale Closure)

这是所有 React 新手的噩梦。

场景:你想写一个定时器,每秒打印一下当前的 count 值。

❌ 翻车现场:

scss 复制代码
const [count, setCount] = useState(0);

useEffect(() => {
  const timer = setInterval(() => {
    // 💀 恐怖故事:这里永远打印 0
    console.log('Current Count:', count);
  }, 1000);

  return () => clearInterval(timer);
}, []); // 依赖数组为空,effect 只跑一次

为什么? 因为 useEffect 执行的那一瞬间(Mount 时),它捕获了当时的 count(也就是 0)。就像拍了一张照片,照片里的人永远定格在那一刻。哪怕外面 count 变成了 100,定时器闭包里的 count 还是 0。

✅ useRef 救场:

我们要用 useRef 造一个"时光胶囊",永远保存最新的值。

const 复制代码
// 1. 创建一个胶囊
const countRef = useRef(count);

// 2. 每次渲染,都把最新的值塞进胶囊里
// 注意:修改 ref 不会触发渲染,所以这里很安全
countRef.current = count;

useEffect(() => {
  const timer = setInterval(() => {
    // 3. 定时器里读胶囊里的值,而不是读外面的快照
    console.log('Current Count:', countRef.current); 
  }, 1000);

  return () => clearInterval(timer);
}, []); // 依然不需要依赖 count,定时器也不用重启

这就是 useRef 的"穿透"能力。它打破了闭包的限制,让你在旧的 Effect 里读到了新的 State。

骚操作二:记录"上一次"的值 (usePrevious)

在 Class 组件时代,我们有 componentDidUpdate(prevProps),可以很方便地对比新旧数据。 到了 Hooks 时代,官方竟然没给这个功能?

别急,useRef 既然能存值,那就能存"前任"。

手写一个 usePrevious Hook:

function 复制代码
  // 创建一个 ref 来存储值
  const ref = useRef();

  // 每次渲染后,把当前值存进去
  // 注意:useEffect 是在渲染*之后*执行的
  useEffect(() => {
    ref.current = value;
  }, [value]);

  // 返回 ref 里的值
  // 注意:也就是在本次渲染时,ref.current 还是*上一次*存进去的值
  return ref.current;
}

// 使用
const Demo = () => {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>现在是: {count}</p>
      <p>刚才还是: {prevCount}</p>
    </div>
  );
};

原理分析:

  1. Render 1 (count=0) : usePrevious 返回 undefined。Render 结束,Effect 运行,ref.current 变为 0。
  2. Render 2 (count=1) : usePrevious 返回 ref.current (也就是 0)。Render 结束,Effect 运行,ref.current 变为 1。

你看,不需要任何魔法,只是利用了 React 的执行顺序,就实现了"时光倒流"。


骚操作三:防止"初次渲染"执行 Effect

有时候,我们希望 useEffect 只有在依赖变化时执行,而不要在组件刚挂载(Mount)时执行。

比如:用户修改搜索词时发请求,但刚进页面时不要发。

const 复制代码
useEffect(() => {
  // 如果是第一次,把开关关掉,直接 return,啥也不干
  if (isFirstMount.current) {
    isFirstMount.current = false;
    return;
  }

  // 从第二次开始,这里的逻辑才会执行
  console.log('搜索词变了,发起请求...');
}, [query]);

这简直就是控制 Effect 执行时机的最强"阀门"。


总结:使用 useRef 的红线

虽然 useRef 很爽,既能穿透闭包,又能静默更新,但请记住一条铁律

永远不要在渲染期间(Rendering Logic)读取或写入 ref.current

const 复制代码
  const count = useRef(0);
  
  // ❌ 报错警告!这是不纯的操作!
  // 在渲染过程中修改 ref,会导致行为不可预测
  count.current = count.current + 1; 

  // ❌ 也不要直接读来做渲染判断
  // 因为 ref 变了不会触发重绘,视图可能不会更新
  return <div>{count.current}</div>;
};

正确的使用姿势:

  • useEffect 里读/写。
  • Event Handler(点击事件等)里读/写。
  • 总之,别在 return JSX 之前的那个函数体里直接搞事。

useRef 是 React 留给我们的后门,当你发现 useState 让你的组件频繁渲染卡顿,或者 useEffect 的依赖数组让你头秃时,不妨想想这个静音的小盒子。

好了,收工。

下期预告 :你真的以为你会写 useCallbackuseMemo 吗?我打赌你的代码里 80% 的 useMemo 都在做负优化。下一篇,我们来聊聊 React 性能优化的"安慰剂效应"。

相关推荐
yinuo1 小时前
前端跨页面通讯终极指南③:LocalStorage 用法全解析
前端
隔壁的大叔1 小时前
正则解决Markdown流式输出不完整图片、表格、数学公式
前端·javascript
胡楚昊1 小时前
CTF SHOW逆向
java·服务器·前端
拉不动的猪1 小时前
前端JS脚本放在head与body是如何影响加载的以及优化策略
前端·javascript·面试
shykevin1 小时前
Actix-Web完整项目实战:博客 API
前端·数据库·oracle
lichong9511 小时前
RelativeLayout 根布局里有一个子布局预期一直展示,但子布局RelativeLayout被 覆盖了
android·java·前端
Tzarevich1 小时前
从字面量到原型链:JavaScript 面向对象的完整进化史
javascript·设计模式
LengineerC1 小时前
我写了一个VSCode的仿Neovide光标动画
前端·visual studio code
WangHappy1 小时前
Mongoose操作MongoDB数据库(1):项目创建与连接配置
前端·mongodb