今日重点
useRef 是 React 为声明式编程留的唯一后门。当 useState 搞不定(要碰 DOM、要存不渲染的变量、要持有 Worker),就用它。核心就一件事:返回一个 { current: xxx } 对象,改 .current 不触发渲染。
DOM 编程之痛
知识点
React 之前,前端靠 document.querySelector 手写 DOM 操作。JS 在 V8 引擎,DOM 在渲染引擎,每次操作都要跨引擎通信,开销大。React 的解法:你只管改数据(useState),DOM 更新框架全包了。
代码
dart
// 被淘汰的方式:命令式 DOM 编程
const h1 = document.querySelector('h1')
h1.innerText = '你好' // 跨引擎通信
h1.style.color = 'red' // 又跨一次
// React 的方式:声明式
const [msg, setMsg] = useState('你好')
// <h1>{msg}</h1>
setMsg('world') // 只改数据,React 算 diff,批量更新 DOM
拆解
- 命令式:每一步都亲自指挥浏览器,数据散落在 DOM 节点上
- 声明式:只声明"值变了",React 接管所有 DOM 更新
为什么这样设计
跨引擎通信是前端性能瓶颈。React 把 DOM 操作内聚到框架层,批量 + diff 优化,开发者不再直接碰 DOM。但总有事必须碰 DOM(聚焦、播放、滚动),这时才用 useRef。
useRef + DOM
知识点
useRef 绑定 DOM 节点,三步走:useRef(null) 创建盒子 → ref={} 绑定标签 → useEffect 里 .current 拿 DOM。
代码
ref-focus-demo/src/App.jsx:9,16,22
scss
const inputRef = useRef(null) // ① 创建空盒子
useEffect(() => {
inputRef.current.focus() // ③ 挂载后 DOM 就绪,调用聚焦
}, [])
<input ref={inputRef} /> // ② 绑定到标签
拆解
useRef(null)返回{ current: null },初始 null 因为此时 input 还没渲染ref={inputRef}是 React 特殊语法,渲染完成后自动把真实 DOM 塞进.currentuseEffect([], [])在挂载后执行,此时.current已是真实 DOM,调.focus()有效
为什么这样设计
React 组件是函数,渲染期间 DOM 还不存在。必须等渲染完、DOM 挂上之后才能操作。useEffect 空依赖正好在这个时间点执行。
存值不渲染
知识点
useRef 不仅能存 DOM,还能存任何值。.current 改了,值确实变了,但 React 不知道,页面不更新。需要借 useState 手动触发渲染。
代码
ref-focus-demo/src/App.jsx:37-43
scss
const numRef = useRef(0) // 初始 0
const [, forceRender] = useState(0) // 只借 setter,不读值
console.log(numRef.current) // 每次渲染打印
<div onClick={() => {
numRef.current += 1 // 改值,React 不感知
forceRender() // 手动触发渲染
}}>
{numRef.current} {/* 渲染时才读到最新值 */}
</div>
拆解
numRef.current += 1:值从 0 变 1、2、3,控制台能看到,但页面不动forceRender():借用 useState 的渲染能力,逼 React 重新执行函数- 函数重新执行 →
{numRef.current}重新读 → 显示最新值
为什么这样设计
有些变量不需要显示在页面上,但需要跨渲染保持(定时器 ID、上一次的值、Worker 引用)。用 useState 每次改都渲染一次浪费性能,用 useRef 刚刚好。
不是响应式
知识点
useState 是响应式的(改了自动渲染),useRef 不是(改了没反应)。这是两者最本质的区别。
代码
scss
// useState:响应式
const [count, setCount] = useState(0)
// setCount(1) → 自动渲染,页面更新
// useRef:非响应式
const countRef = useRef(0)
// countRef.current = 1 → 值变了,页面不动
拆解
- useState 的 setter 会通知 React "值变了,重渲染"
- useRef 的
.current就是一个普通对象属性,改了 React 完全不知道 - 存 DOM、存 Worker、存定时器 ID 用 ref,显示在页面上的用 state
为什么这样设计
两个 Hook 各司其职:state 管前台(用户看得见的),ref 管后台(纯存储)。如果 ref 也响应式,就没法存"不想渲染的变量"了。
Worker ref
知识点
JS 是单线程的。复杂计算(大量循环、LLM、游戏逻辑)会卡住主线程,页面冻结。Web Worker 另开一条线程,重活扔过去,主线程继续响应用户交互。useRef 用来持有 Worker 实例。
代码
ref-worker-demo/src/App.jsx:20-27
javascript
const workerRef = useRef(null)
useEffect(() => {
workerRef.current = new Worker(
new URL('./worker.js', import.meta.url)
)
}, [])
ref-worker-demo/src/worker.js:1
arduino
console.log('worker线程启动我来了')
拆解
workerRef.current = new Worker(...):手动把 Worker 实例存入 refnew URL('./worker.js', import.meta.url):Vite 的模块化 Worker 加载方式- 放 useEffect 里:不在渲染期间创建(让路),空依赖确保只创建一次
为什么这样设计
- Worker 对象不需要显示,用 ref 不用 state
- 不在函数体顶层
new Worker():会每次渲染都创建新实例,内存泄漏 - 放 useEffect 空依赖:挂载后创建一次,卸载时
terminate()销毁
.current 设计
知识点
为什么 useRef 不直接返回值,非要用 .current 包一层?因为 JS 里只有对象是引用类型,普通值在函数重新执行时会重置。
代码
csharp
// 如果不用对象包一层:
function App() {
let num = 0 // 每次渲染重新声明,永远是 0
num += 1 // 从 0 变 1,下次渲染又重置为 0
}
// useRef 的做法:
const numRef = useRef(0) // { current: 0 }
// 对象地址不变 → 改 .current 不会丢 → 持久化
拆解
- React 组件每次渲染函数都重新执行,普通变量全部重置
- 对象是引用类型,只要地址不变,改属性是持久的
{ current: 0 }的盒子始终是同一个,只是里面的值在变
为什么这样设计
不是 React 故意搞复杂,是 JS 语言本身的限制。返回对象是唯一让值跨渲染持久化的方式。
自查清单
- React 为什么要消灭 DOM 编程? --- 见「DOM 编程之痛」
- useRef 绑定 DOM 的三步是什么?为什么初始值是 null? --- 见「useRef + DOM」
- ref 存值改了为什么页面不更新?怎么让它更新? --- 见「存值不渲染」
- useRef 和 useState 的本质区别? --- 见「不是响应式」
- 为什么 new Worker 要放 useEffect 里而不是函数顶层? --- 见「Worker ref」
- 为什么 useRef 要用 .current 包一层? --- 见「.current 设计