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

前言:它不仅仅是 document.getElementById

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

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

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

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


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

首先,你得把 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 的依赖数组让你头秃时,不妨想想这个静音的小盒子。

好了,收工。

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

相关推荐
鬼手点金2 小时前
nanoGPT/nanochat 能做出 DeepSeek/Qwen 这样的大模型吗?
服务器·前端·javascript·bug·openclaw
蛋蛋的就会顺顺的3 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
程序员Sunday3 小时前
TypeScript 的 any、unknown、never 有什么区别?
前端·面试·typescript
朱涛的自习室3 小时前
70 美金的 Beyond Compare,我让 Gemini 4 和 DeepSeek 造个开源免费版(Day 2)
android·前端·人工智能
码艺-Alimjan4 小时前
Tauri 源码备份 · 打包 & 解压 总结,源码收集之路
前端·javascript·vue.js
小马不起床4 小时前
零基础玩转 FastAPI_11:前端后台管理页面
前端·状态模式·fastapi
子非鱼a5 小时前
【文件上传】一个普普通通的edu16
前端
美狐美颜SDK开放平台6 小时前
直播APP如何实现美白滤镜功能?视频美颜SDK技术开发方案详解
前端·数据库·人工智能·音视频·美颜sdk·直播美颜sdk·视频美颜sdk
pride.li6 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
屏什么录7 小时前
电脑如何录屏幕视频和声音?从工具到代码,7 种方案帮你彻底搞定
前端·音视频开发