一篇文章搞懂 useRef:聚焦 DOM、模拟 forceRender、管理 Worker 都靠它

一篇文章搞懂 useRef:聚焦 DOM、模拟 forceRender、管理 Worker 都靠它

React 里最被低估的 Hook,除了绑定 DOM 还能干这么多事。

前言

很多 React 新手对 useRef 的印象停留在"拿来聚焦输入框",然后就没了。其实 useRef 的能力远不止于此------它能存可变值不触发渲染、能模拟 forceRender、还能管理 Web Worker 线程。

这篇文章带你从三个真实场景出发,彻底搞懂 useRef 的底层逻辑。文末附有完整可运行项目代码,克隆即可上手。

适合读者 :会用 useState 但还没真正理解 useRef 的 React 初学者。

项目概览

ref-demo 包含两个子项目,覆盖了 useRef 的三种典型用法:

csharp 复制代码
ref-demo/
├── readme.md              ← 知识点笔记(DOM 编程、响应式、Web Worker)
├── ref-focus-demo/        ← useRef 操作 DOM + forceRender 模式
│   └── src/App.jsx
└── ref-worker-demo/       ← useRef 持有 Web Worker 实例
    ├── src/App.jsx
    └── src/worker.js
子项目 核心知识点
ref-focus-demo useRef 绑定 DOM、useRef vs useState(响应式 vs 非响应式)、forceRender 模式
ref-worker-demo useRef 持有 Web Worker、JS 单线程 / event loop、主线程与 Worker 通信

依赖版本:React 19.2.6、Vite 8.0.12


核心知识点一:useRef 是什么?为什么有了 useState 还要它?

一句话理解

useRef 返回一个可变的"容器对象" ,这个对象有一个 current 属性,你可以往里面放任何值,修改它不会触发组件重新渲染

js 复制代码
const ref = useRef(null)
// ref 就是一个普通对象:{ current: null }
ref.current = 'hello'
// ref.current 现在是 'hello',但组件不会重新渲染

类比

概念 类比
useState 冰箱上的便利贴------内容变了,全家人都能看到(触发渲染)
useRef 你口袋里的记事本------记了东西,只有你知道,别人不知道(不触发渲染)

useState vs useRef 对比

jsx 复制代码
// useState --- 响应式:改值 → 重渲染
const [count, setCount] = useState(0)
setCount(1)  // ✅ 页面自动更新

// useRef --- 非响应式:改值 → 不渲染
const countRef = useRef(0)
countRef.current = 1  // ❌ 页面不会更新

React 的设计哲学 :90% 的场景用响应式 useState 驱动 UI,10% 的边缘场景用 useRef。两者配合,不是二选一。


核心知识点二:useRef 操作 DOM --- autoFocus 替代方案

场景

输入框一出现就自动聚焦,不用鼠标点。HTML 有 autoFocus 属性,但 useRef + useEffect 更灵活------你可以在任何时机触发聚焦。

代码

jsx 复制代码
import { useRef, useEffect } from 'react'

const App = () => {
  const inputRef = useRef(null)  // ① 创建 ref 容器

  useEffect(() => {
    inputRef.current.focus()     // ③ 挂载后,ref.current 指向真实 DOM,手动聚焦
  }, [])

  return (
    <input
      ref={inputRef}             // ② 绑定到 JSX 元素
      type="text"
      placeholder="请输入用户名"
    />
  )
}

这段代码的执行时序

csharp 复制代码
组件函数执行 → 返回 JSX → React 创建 DOM 节点 → 挂载到页面
     ↑                                                    ↓
 如果在这里 ref.current.focus()                  ref.current 这时候才被赋值
 → ref.current 还是 null,报错!                          ✅
步骤 发生了什么 ref.current 的值
组件函数执行 调用 useRef(null),创建容器 null
JSX 返回 声明了 <input ref={inputRef}> 还是 null(DOM 还没创建)
React commit 真实 <input> 挂载到页面 React 把 DOM 节点赋值给 ref.current
useEffect 回调 此时 DOM 已存在,安全调用 .focus() 指向 HTMLInputElement

底层逻辑

为什么必须用 useEffect?因为组件函数执行时 DOM 还不存在------JSX 只是"装修图纸",React 需要先按图纸造房子(创建 DOM),房子造好之后才会执行 useEffect

autoFocus 不用 useEffect 是因为它是浏览器原生属性,浏览器自己会在元素创建后处理------不依赖 JS 调用时序。

注释代码里还有另一种方案

项目中被注释掉的代码展示了 useRef + useState 结合的版本:

jsx 复制代码
// 注释版本:ref 绑定 input + useState 驱动计数
const [count, setCount] = useState(0)
const inputRef = useRef(null)
// inputRef 只管 DOM 聚焦,count 管业务值,各司其职

这是一种"关注点分离"的模式------state 管 UI 数据,ref 管 DOM 操作,互不干扰。


核心知识点三:forceRender 模式 --- 把 useState 当"手动刷新键"

什么场景需要?

你有一些数据不需要触发渲染,但又希望在某些时机手动刷新页面。比如高频更新的计数器、动画帧、实时数据流------如果用 useState 每次更新都重渲染,性能就崩了。

代码(ref-focus-demo 当前版本)

jsx 复制代码
const App = () => {
  const numRef = useRef(0)           // ① 静默记账本
  const [, forceRender] = useState(0) // ② 只拿 setter,state 值本身不用

  return (
    <div onClick={() => {
      numRef.current += 1             // ③ 改值但不渲染
      forceRender(numRef.current)     // ④ 手动触发一次渲染
    }}>
      {numRef.current}                 // ⑤ 渲染时从 ref 里读最新值
    </div>
  )
}

逐行拆解

代码 作用
useRef(0) 创建非响应式容器,存数字
const [, forceRender] = useState(0) 解构时用逗号跳过 state 值,只拿 setter。这个 setter 的意思变了------不再是"更新 state 值",而是**"通知 React 重新渲染"**
numRef.current += 1 静悄悄改值,页面不动
forceRender(numRef.current) 调用 setter → React 重渲染 → 组件重新执行 → numRef.current 读到最新值 → 页面更新

为什么不直接用 useState?

jsx 复制代码
// useState 版本:每次点击触发渲染
const [num, setNum] = useState(0)
setNum(num + 1)  // 改值 + 触发渲染,两步合一

// useRef + forceRender 版本:改值和渲染解耦
numRef.current += 1      // 只改值
forceRender(...)         // 只触发渲染

普通场景下 useState 就够。但当你需要在不渲染的情况下多次改值,然后攒一波统一刷新时,forceRender 模式就派上用场了。

更实用的场景

jsx 复制代码
// 高频计数器:每秒更新 10 次值,但每 5 次才渲染一次
const countRef = useRef(0)
const [, forceRender] = useState(0)

useEffect(() => {
  const timer = setInterval(() => {
    countRef.current += 1
    if (countRef.current % 5 === 0) {
      forceRender(countRef.current)  // 省掉 4/5 的渲染开销
    }
  }, 100)
  return () => clearInterval(timer)
}, [])

核心知识点四:useRef 持有 Worker 实例

JS 为什么需要 Worker?

JS 是单线程 的------所有代码跑在一条线上。页面上既要响应用户点击、滚动,又要执行计算任务,如果有耗时操作(大循环、AI 推理、游戏逻辑),页面就会卡死

Web Worker 是浏览器提供的方案:另开一条线程,把耗时任务丢给它,完成后通过消息通知主线程。

代码(ref-worker-demo)

主线程 App.jsx

jsx 复制代码
const App = () => {
  const workerRef = useRef(null)  // ① 持久化持有 Worker 引用

  useEffect(() => {
    workerRef.current = new Worker(
      new URL('./worker.js', import.meta.url)  // ② 创建 Worker 实例
    )
  }, [])

  return <></>
}

Worker 线程 worker.js

js 复制代码
console.log('work online')

为什么要用 useRef 存 Worker?

用 useState 用 useRef
Worker 实例触发渲染 ← 没意义,没必要 Worker 实例只存不渲染 ← 合理
每次 setState 通知 React 重绘 ← 浪费 不触发渲染,干干净净

Worker 是一个"后台干活的东西",跟 UI 没关系 ,所以天然适合用 useRef 来持有。

注释代码里的知识点

项目中注释掉了一段阻塞主线程的代码:

js 复制代码
// console.time('主线程耗时')
// for (let i = 0; i < 1000000; i++) {
//   console.log(i)
// }
// console.timeEnd('主线程耗时')

这段循环如果执行,会占满主线程,页面上的按钮、滚动、输入框全部无响应------直到循环结束。而 Worker 线程里的计算不会影响主线程,这就是 Worker 的价值。


总结

回顾这篇文章的核心收获:

  1. useRef 返回一个 { current } 容器 ,改 current 不触发渲染------这是它和 useState 的本质区别
  2. 操作 DOM 用 useRef + useEffectref 拿到 DOM 节点,useEffect 保证在 DOM 挂载后执行
  3. 响应式 vs 非响应式不是对立,是互补 :90% 用 useState 驱动 UI,10% 用 useRef 处理边缘场景
  4. forceRender 模式useState setter 的本质是"通知 React 重新渲染",可以和 useRef 配合实现"改值和渲染解耦"
  5. useRef 存 Worker 实例:跟 UI 无关的东西,放 ref 里最合适

下一步推荐学习:useRef 配合 forwardRef 实现父子组件间的 DOM 透传,以及 useImperativeHandle 自定义暴露给父组件的实例方法。

你平时用 useRef 最多干什么?评论区聊聊 👋


完整项目代码

项目地址:gitee.com/dcx2758/ai_...

项目文件树

csharp 复制代码
ref-demo/
├── readme.md                         # 知识点笔记
├── ref-focus-demo/
│   ├── package.json                  # React 19.2.6 + Vite 8.0.12
│   ├── src/
│   │   ├── main.jsx                  # 入口,StrictMode
│   │   ├── App.jsx                   # useRef DOM 聚焦 + forceRender
│   │   ├── App.css
│   │   └── index.css
│   └── vite.config.js
└── ref-worker-demo/
    ├── package.json                  # React 19.2.6 + Vite 8.0.12
    ├── src/
    │   ├── main.jsx                  # 入口,StrictMode
    │   ├── App.jsx                   # useRef 持有 Worker 实例
    │   ├── worker.js                 # Worker 线程代码
    │   ├── App.css
    │   └── index.css
    └── vite.config.js

快速启动

bash 复制代码
# 克隆仓库
git clone git@gitee.com:dcx2758/ai_doubao_dcx.git
cd ai_doubao_dcx/dcx/fe/react/basic/ref-demo

# 启动 ref-focus-demo(useRef 聚焦 DOM + forceRender)
cd ref-focus-demo
npm install
npm run dev

# 启动 ref-worker-demo(useRef 持有 Worker)
cd ../ref-worker-demo
npm install
npm run dev
相关推荐
栀鸢ouo36 分钟前
useTableHeight:一个优雅的 Vue 3 表格自适应高度解决方案
前端
前端Hardy39 分钟前
Vue 终于杀进终端界!这个开源项目让 CLI 开发像写网页一样简单
前端·javascript·后端
一次旅行1 小时前
多智能体编排实战:拆解Plan-and-Execute范式+三层记忆架构,手写无依赖轻量Agent调度引擎
前端·javascript·架构
小玮看世界2 小时前
[Python]从“脏”数据到优雅实现:一个IoT滑动窗口最大值问题的测试驱动优化实录
linux·前端·python
尤乐娃子3 小时前
进入大厂(厂子大)实习Day11
前端·笔记·实习
xiaoxiangsiyan3 小时前
运维之前端反调试学习
运维·前端·学习·状态模式
小徐_23333 小时前
TRAE WORK 实战,之前写一篇水文要半天,现在用 TRAE Work 摸鱼2分钟交差,真香!
前端·trae
明月_清风3 小时前
🤗 Hugging Face 模型上传完全指南:从本地到 Hub 的 4 种姿势
前端·后端·ai编程