一句话总结:useRef 是 React 提供的一个「魔法盒子」📦,它能帮你保存任何东西,而且换包装(重新渲染)时里面的东西不会变!
📚 目录
- 1. 什么是 useRef?
- 2. useRef vs useState:傻傻分不清?
- 3. 实战一:让输入框自动聚焦 🔍
- 4. 实战二:管理 Web Worker 线程 ⚡
- 5. useRef 的核心特性总结
- 6. 什么时候该用 useRef?
1. 什么是 useRef?
🎭 先来个比喻
想象你有一个神奇的盒子:
📦 useRef(初始值)
│
├── 📝 盒子标签:ref.current
│
└── ✨ 特殊能力:
├── 换包装(组件重新渲染)时,里面的东西不会丢
├── 改变里面的东西时,不会触发重新包装(不触发渲染)
└── 可以装任何东西:DOM 元素、数字、对象、Worker 线程…
💻 基本语法
import { useRef } from 'react';
// 创建一个 ref 对象,初始值为 null
const myRef = useRef(null);
// 访问 ref 的值
console.log(myRef.current); // null
// 修改 ref 的值(不会触发重新渲染!)
myRef.current = '新值';
console.log(myRef.current); // '新值'
📦 内部结构长这样
// useRef(initialValue) 返回的对象:
{
current: initialValue // ← 这就是那个「神奇盒子」里的东西
}
2. useRef vs useState:傻傻分不清?🤔
这是新手最容易混淆的地方,让我们用一张表说清楚:
| 修改后会重新渲染? | ✅ 会! | ❌ 不会! |
| 适合存什么? | 需要展示在界面上的数据 | 不需要展示的数据、DOM 引用、定时器等 |
| 更新方式 | setState(newValue) | ref.current = newValue |
| 渲染次数 | 每次 setState 都触发渲染 | 修改不触发渲染 |
🎬 直观对比
const App = () => {
const [count, setCount] = useState(0); // 🔄 响应式数据
const numRef = useRef(0); // 📦 可变对象
const [, forceRender] = useState(0); // 用于强制渲染
console.log('组件渲染了!');
return (
<>
{/* useState:点击后自动重新渲染 */}
<button onClick={() => setCount(count + 1)}>
useState: {count} {/* 🔄 数字会更新 */}
</button>
{/* useRef:点击后不会自动重新渲染 */}
<button onClick={() => {
numRef.current += 1;
console.log('ref 的值:', numRef.current); // 📦 值变了
forceRender(n => n + 1); // 手动触发渲染才能看到变化
}}>
useRef: {numRef.current} {/* 📦 不手动渲染就不更新 */}
</button>
</>
);
}
🔑 关键区别图解
useState 流程:
setState(新值)
↓
🔄 触发重新渲染
↓
界面更新 ✨
useRef 流程:
ref.current = 新值
↓
📦 值已保存
↓
❌ 不触发渲染
↓
界面不变(除非手动触发)
3. 实战一:让输入框自动聚焦 🔍
🎯 需求
用户打开页面后,输入框自动获得焦点,不用手动点击。
💡 为什么需要 useRef?
React 的理念:不直接操作 DOM
↓
但有时候必须操作 DOM(比如 focus)
↓
怎么办?用 useRef 获取 DOM 引用!
📝 完整代码解析
import { useRef, useEffect } from 'react';
const App = () => {
// 第一步:创建 ref 对象,初始值为 null
// 💡 此时 inputRef.current = null,还没绑定任何 DOM
const inputRef = useRef(null);
// 第三步:在组件挂载后,使用 ref 操作 DOM
useEffect(() => {
// 💡 此时 inputRef.current 已经指向 input 元素了
console.log(inputRef.current); // <input type="text" …>
// 调用原生 DOM API,让输入框获得焦点
inputRef.current.focus();
}, []); // 空依赖数组 = 只在挂载时执行一次
return (
<>
<h2>自动聚焦示例</h2>
{/* 第二步:通过 ref 属性,把 DOM 元素绑定到 ref 上 */}
{/* 💡 组件挂载后,inputRef.current 就会指向这个 input 元素 */}
<input
type="text"
placeholder="请输入用户名"
ref={inputRef} // ← 关键!绑定 DOM
/>
</>
);
};
export default App;
🔄 执行流程图解
组件初始化
↓
创建 inputRef = { current: null } 📦
↓
JSX 渲染,创建 <input> 元素
↓
React 发现 ref={inputRef}
↓
自动执行:inputRef.current = <input DOM元素> 🎯
↓
组件挂载完成,触发 useEffect
↓
执行:inputRef.current.focus() ✨
↓
输入框获得焦点!
⚠️ 常见错误
// ❌ 错误:在渲染时访问 ref
const App = () => {
const inputRef = useRef(null);
// 此时 inputRef.current 还是 null!DOM 还没创建
console.log(inputRef.current); // null
inputRef.current.focus(); // 💥 报错!
return <input ref={inputRef} />;
};
// ✅ 正确:在 useEffect 中访问 ref
const App = () => {
const inputRef = useRef(null);
useEffect(() => {
// 此时 DOM 已经创建,ref 已经绑定
inputRef.current.focus(); // ✅ 正常工作
}, []);
return <input ref={inputRef} />;
};
4. 实战二:管理 Web Worker 线程 ⚡
🎯 需求
执行一个非常耗时的计算任务(50 亿次循环),但不能卡死页面。
🤔 为什么需要 Web Worker?
JavaScript 是单线程的
↓
主线程负责:脚本执行 + DOM 渲染 + 用户交互
↓
如果在主线程执行 50 亿次循环…
↓
💥 页面卡死!用户无法操作!
解决方案:把任务交给 Web Worker 子线程
↓
Worker 在后台计算,主线程继续响应用户
↓
计算完成后,Worker 通知主线程
📝 完整代码解析
主线程代码(App.jsx)
import { useEffect, useRef, useState } from 'react';
function App() {
// 🔑 关键:用 useRef 保存 Worker 实例
// 为什么不用 useState?
// 1. 修改 Worker 不需要触发重新渲染
// 2. 需要在组件重新渲染时保持同一个 Worker 实例
const workerRef = useRef(null);
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 第一步:创建 Worker 线程
// 💡 开销比较大,所以放在 useEffect 中,只创建一次
workerRef.current = new Worker(
new URL('./worker.js', import.meta.url)
);
// 第二步:监听 Worker 的消息
workerRef.current.onmessage = (e) => {
console.log('收到 worker 消息:', e.data);
setLoading(false);
setResult(e.data.result);
};
// 第三步:组件卸载时,销毁 Worker 线程
// 💡 这很重要!否则会造成内存泄漏
return () => {
workerRef.current.terminate(); // 终止 Worker
workerRef.current = null; // 手动回收
};
}, []); // 空依赖 = 只在挂载时创建一次
// 发送任务给 Worker
const startHeavyCalc = () => {
setLoading(true);
// 通过 postMessage 发送消息给 Worker
workerRef.current.postMessage({ num: 88 });
};
return (
<div style={{ padding: "30px" }}>
<h2>useRef + WebWorker 耗时运算</h2>
<p>开启 web worker 线程,执行50亿次循环,结束后通知主线程</p>
<button onClick={startHeavyCalc} disabled={loading}>
{loading ? "正在计算…" : "启动繁重计算任务"}
</button>
{result && <h3>计算结果: {result}</h3>}
</div>
);
}
export default App;
Worker 线程代码(worker.js)
// 📌 Web Worker 独立子线程
// ⚠️ 注意:Worker 中不能访问 DOM!
console.log('worker online');
// self 是 Worker 的全局对象(类似主线程的 window)
self.onmessage = (e) => {
const { num } = e.data;
console.log('worker 收到主线程任务,参数为', e.data);
// 模拟耗时计算:50 亿次循环
let sum = 0;
for (let i = 0; i < 5000000000; i++) {
sum += num * i;
}
// 计算完成,通知主线程
self.postMessage({
result: sum
});
};
🔄 完整流程图解
┌─────────────────────────────────────────────────────────────┐
│ 主线程 (Main Thread) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 组件渲染 │ → │ 创建 Worker │ → │ 等待结果… │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↑ │ │
│ │ ↓ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 界面更新 │ ←─────────────────── │ 收到消息 │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ 消息通信
┌─────────────────────────────────────────────────────────────┐
│ Worker 线程 (Worker Thread) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 收到任务 │ → │ 执行计算 │ → │ 发送结果 │ │
│ │ {num: 88} │ │ 50亿次循环 │ │ {result} │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
🎯 为什么用 useRef 保存 Worker?
// ❌ 错误:用 useState 保存 Worker
const [worker, setWorker] = useState(null);
useEffect(() => {
const w = new Worker(…);
setWorker(w); // 触发重新渲染…
// 💥 每次渲染都会创建新的 Worker!
}, []);
// ❌ 错误:直接在组件中创建 Worker
const worker = new Worker(…); // 💥 每次渲染都创建新 Worker!
// ✅ 正确:用 useRef 保存 Worker
const workerRef = useRef(null);
useEffect(() => {
workerRef.current = new Worker(…); // 只创建一次
// useRef 不会触发重新渲染
// 组件重新渲染时,workerRef.current 保持不变
}, []);
5. useRef 的核心特性总结
🎁 特性一:持久化存储
const countRef = useRef(0);
// 组件第一次渲染
console.log(countRef.current); // 0
countRef.current = 1;
// 组件第二次渲染(比如 state 变化触发)
console.log(countRef.current); // 1 ✨ 值还在!
// 组件第三次渲染
console.log(countRef.current); // 1 ✨ 还在!
🎁 特性二:不触发渲染
const renderCount = useRef(0);
// 每次渲染时,renderCount.current 加 1
renderCount.current += 1;
// 💡 这里不会无限循环!因为修改 ref 不会触发渲染
console.log(`组件渲染了 ${renderCount.current} 次`);
🎁 特性三:引用 DOM 元素
const divRef = useRef(null);
useEffect(() => {
// 访问 DOM 元素的属性和方法
console.log(divRef.current.offsetWidth); // 宽度
console.log(divRef.current.offsetHeight); // 高度
console.log(divRef.current.classList); // CSS 类名
divRef.current.scrollIntoView(); // 滚动到可见区域
}, []);
return <div ref={divRef}>Hello</div>;
🎁 特性四:保存定时器 ID
const timerRef = useRef(null);
const startTimer = () => {
// 保存定时器 ID,以便后续清除
timerRef.current = setInterval(() => {
console.log('tick');
}, 1000);
};
const stopTimer = () => {
// 使用保存的 ID 清除定时器
clearInterval(timerRef.current);
};
6. 什么时候该用 useRef?✅
✅ 适合使用 useRef 的场景
📌 需要访问 DOM 元素
└── focus()、scrollIntoView()、测量尺寸等
📌 需要保存定时器 ID
└── setTimeout、setInterval 的返回值
📌 需要保存上一次的值
└── 用于对比前后状态
📌 需要保存不需要渲染的值
└── 计数器、标志位、缓存等
📌 需要保存大型对象实例
└── Web Worker、WebSocket、第三方库实例等
❌ 不适合使用 useRef 的场景
❌ 需要展示在界面上的数据
└── 用 useState
❌ 数据变化后需要更新界面
└── 用 useState
❌ 需要传递给子组件并响应变化
└── 用 useState 或 useContext
🎯 快速判断流程
这个数据需要显示在界面上吗?
│
├── 是 → 用 useState
│
└── 否 → 数据变化时需要触发渲染吗?
│
├── 是 → 用 useState
│
└── 否 → 用 useRef ✅
🎉 总结
useRef 的本质
useRef = 一个不会触发渲染的「魔法盒子」📦
│
├── 可以装任何东西
├── 换包装(渲染)时里面的东西不会丢
└── 改变里面的东西时不会触发换包装
一句话记忆法
「存东西用 useRef,改界面用 useState」
核心要点
| 📦 本质 | 一个持久化的可变对象 { current: value } |
| 🔄 渲染 | 修改 .current 不会触发重新渲染 |
| 🎯 DOM | 通过 ref 属性绑定 DOM 元素 |
| ⏰ 生命周期 | 组件卸载后,ref 的值会丢失 |
| 💡 使用场景 | DOM 操作、定时器、Worker、缓存等 |
📚 延伸阅读
- React 官方文档 – useRef
- React 官方文档 – 操作 DOM
- MDN – Web Workers API
💡 学习建议:动手实践是最好的学习方式!尝试运行 ref-focus-demo 和 ref-worker-demo 两个示例项目,亲身体验 useRef 的魔力!

