前言:它不仅仅是 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>
);
};
原理分析:
- Render 1 (count=0) :
usePrevious返回undefined。Render 结束,Effect 运行,ref.current变为 0。 - 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 性能优化的"安慰剂效应"。