React useRef 实战:从 input 聚焦到 Web Worker 引用
本文基于本地 React ref-demo 静态源码整理,代码部分运行未验证。文章重点解决两个初学者问题:
useRef绑定 DOM 后发生什么,以及为什么 Worker 实例适合保存到 ref 中。
标签:React, React Hooks, useRef, Web Worker
一、先建立最小理解
React 中的 useRef 不只是"获取 DOM"的工具。它返回一个稳定对象,对象的值放在 current 属性中。React 会在多次渲染之间保留这个对象,但修改 current 不会自动触发页面重新渲染。
这组练习包含两个场景:第一个场景把 ref 绑定到 input,并在组件挂载后调用 focus();第二个场景把 Web Worker 实例放进 ref,避免组件重新渲染时重复创建线程。
二、useRef 到底保存了什么?
jsx
const inputRef = useRef(null)
可以先把它理解成:
js
{
current: null
}
初始值是 null,因为组件刚执行时,input 还没有挂载到真实 DOM。绑定之后:
jsx
<input ref={inputRef} placeholder="请输入用户名" />
React 会在 input 挂载后,让:
js
inputRef.current
指向真实的 HTMLInputElement 对象。此时就可以调用:
js
inputRef.current.focus()
inputRef.current.value
inputRef.current.blur()
这里需要注意:ref={inputRef} 是 JSX 属性,不是普通字符串。React 负责在挂载和卸载阶段维护 current 的引用。
三、为什么 DOM 操作要放在 useEffect?
项目中的聚焦逻辑是:
jsx
const inputRef = useRef(null)
useEffect(() => {
console.log(inputRef.current)
inputRef.current.focus()
}, [])
执行顺序可以简化为:
text
执行组件函数
↓
current 初始为 null
↓
React 创建并挂载 input
↓
current 指向 input DOM
↓
useEffect 执行 focus()
因此,如果在组件函数执行阶段直接调用 inputRef.current.focus(),可能因为 current 还是 null 而无法操作 DOM。
如果只是首次加载时聚焦,也可以写:
jsx
<input autoFocus placeholder="请输入用户名" />
区别在于:autoFocus 是声明式的初次聚焦;ref 可以在按钮点击、弹窗打开或异步请求结束后动态调用 focus()。
四、useRef 和 useState 如何选择?
项目中还有一个保存数字的练习:
jsx
const numRef = useRef(0)
const [, forceRender] = useState(0)
<div onClick={() => numRef.current++ + forceRender()}>
<h1>{numRef.current}</h1>
</div>
点击时,numRef.current++ 修改了 ref,但它本身不会让页面更新;forceRender() 才触发了重新渲染,所以新的数字才会显示出来。
这个例子适合用来理解机制,不推荐作为实际业务写法。判断标准如下:
| 数据特点 | 推荐方案 |
|---|---|
| 改变后需要更新页面 | useState |
| DOM 节点引用 | useRef |
| Worker、定时器或 WebSocket 实例 | useRef |
| 需要展示的计算结果 | useState |
一句话:State 管页面,Ref 管引用。
五、Web Worker 是怎么创建的?
项目中创建 Worker 的代码是:
jsx
const workerRef = useRef(null)
useEffect(() => {
workerRef.current = new Worker(
new URL('./worker.js', import.meta.url)
)
}, [])
真正创建新线程的是:
js
new Worker(...)
并不是 useRef。useRef 只负责保存 Worker 实例,方便后续调用:
js
workerRef.current.postMessage(data)
workerRef.current.terminate()
new URL 的含义
js
new URL('./worker.js', import.meta.url)
它表示:以当前模块文件的地址为基准,定位同目录或相对目录中的 worker.js。在 Vite 项目中,这种写法可以让构建工具处理 Worker 文件路径。
当前项目的 worker.js 只有一条启动日志:
js
console.log('worker 线程启动了')
因此,当前材料只证明了 Worker 的创建意图,尚未形成主线程与 Worker 之间的消息通信闭环。
六、Worker 为什么要配合 useEffect 和清理?
不要在组件函数中直接创建:
jsx
function App() {
const worker = new Worker('./worker.js')
}
组件每次重新渲染时,函数可能再次执行,从而创建多个 Worker。
更完整的资源管理方式是:
jsx
useEffect(() => {
const worker = new Worker(
new URL('./worker.js', import.meta.url)
)
workerRef.current = worker
return () => {
worker.terminate()
workerRef.current = null
}
}, [])
useEffect 创建外部资源,返回的清理函数释放外部资源。这个模式也适用于定时器、事件监听和 WebSocket。
七、常见错误排查表
| 现象 | 原因 | 排查方式 |
|---|---|---|
current 是 null |
DOM 尚未挂载或没有绑定 ref | 检查 ref={inputRef},并在 effect 或事件中访问 |
| 修改 ref 后页面不变 | ref 不会触发重新渲染 | 页面状态改用 useState |
| 每次渲染都创建 Worker | 在组件函数中创建了 Worker | 放进 useEffect |
| Worker 无法加载 | 路径或构建方式不匹配 | 检查 new URL 的相对路径 |
| 页面关闭后 Worker 仍存在 | 没有清理线程 | 卸载时调用 terminate() |
| Worker 没有返回结果 | 未实现消息监听和发送 | 补充 onmessage 与 postMessage |
八、面试中如何介绍这个练习
我通过 React
useRef学习了两类非响应式引用:一类是通过ref获取 input DOM 并在 effect 中调用focus(),另一类是保存 Web Worker 实例。这里new Worker才负责创建线程,ref 只负责跨渲染保存实例;需要展示给用户的计算结果则应该放入 state。资源型对象还要在组件卸载时清理,避免留下 Worker 实例。
九、最后的自检清单
- 能解释
useRef(null)返回的对象结构。 - 能说明 input 挂载前后
current的变化。 - 能区分
autoFocus和ref.current.focus()。 - 能说明为什么页面状态使用
useState。 - 能解释
new Worker和useRef各自的职责。 - 能解释
new URL('./worker.js', import.meta.url)。 - 能写出 Worker 的创建和
terminate()清理流程。 - 能指出当前
worker.js还没有实现消息通信。