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

相关推荐
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte5 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT065 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法