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 性能优化的"安慰剂效应"。

相关推荐
沿着路走到底18 分钟前
JS事件循环
java·前端·javascript
子春一236 分钟前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶42 分钟前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied2 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一22 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
前端无涯2 小时前
React/Vue 代理配置全攻略:Vite 与 Webpack 实战指南
vue.js·react.js
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记