useRef 的三个段位:从 DOM 引用到 Web Worker,彻底搞懂 React 的"非响应式"利器

useRef 的三个段位:从 DOM 引用到 Web Worker,彻底搞懂 React 的"非响应式"利器

useRef 是 React 中最容易被低估的 Hook。很多人只知道它能"获取 DOM",却不知道它还能存储可变对象、持有 Worker 线程、做性能优化。本文从 DOM 编程的性能困境出发,带你逐层深入 useRef 的三个段位------DOM 引用 → 可变对象 → Web Worker 管理,并对比 useState 揭示"响应式 vs 非响应式"的本质差异。全文代码可直接运行,建议收藏后动手实践。


一、为什么 React 要规避 DOM 编程?

1.1 JS 引擎与渲染引擎的分离

浏览器内部有两个核心引擎,它们运行在不同的世界中:

css 复制代码
┌──────────────────────────────────────────┐
│              浏览器进程                    │
│                                          │
│  ┌──────────────┐    ┌──────────────┐   │
│  │  V8 引擎      │    │  渲染引擎     │   │
│  │              │    │              │   │
│  │  执行 JS 代码 │    │  解析 HTML   │   │
│  │  运行业务逻辑 │    │  构建 DOM 树 │   │
│  │  计算和处理   │    │  CSSOM + 渲染│   │
│  └──────┬───────┘    └──────┬───────┘   │
│         │                   │            │
│         └───────桥接────────┘            │
│           (跨引擎通信开销大)               │
└──────────────────────────────────────────┘

JS 操作 DOM 为什么慢?

javascript 复制代码
JS 引擎(V8)              渲染引擎
    │                          │
    │  document.getElementById │
    │  ──────────────────────▶ │  跨引擎调用
    │                          │  查找 DOM 节点
    │  ◀────────────────────── │  返回 DOM 对象
    │                          │
    │  element.style.color     │
    │  ──────────────────────▶ │  跨引擎修改
    │                          │  触发重排/重绘
    │                          │

每次 JS 操作 DOM,都要跨越引擎边界通信------这是 DOM 编程耗性能的根本原因。

1.2 React 的解法:规避 DOM 编程

ini 复制代码
传统 JS 开发(jQuery 时代):
  直接操作 DOM
  document.getElementById('app').innerHTML = '...'
  性能瓶颈在跨引擎通信

React / Vue 开发:
  不直接操作 DOM
  ┌──────────┐    ┌──────────────┐    ┌──────────┐
  │ useState │───▶│ React 虚拟DOM │───▶│ 真实 DOM │
  │ 数据状态  │    │  Diff 算法    │    │  自动更新 │
  └──────────┘    └──────────────┘    └──────────┘
  你只管改数据,React 帮你改 DOM

前端开发方式的根本转变:从"命令式操作 DOM"到"声明式数据驱动"。


二、useRef 第一段位:DOM 引用

2.1 场景:自动聚焦输入框

有些场景必须直接操作 DOM,比如页面加载后自动 focus 输入框------把用户当小白,不用让他们手动点一下。

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

const App = () => {
  const [count, setCount] = useState(0);
  // ref 对象引用,初始值为 null
  // 未来它会引用真实的 DOM 节点
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后,inputRef.current 已指向 input DOM 节点
    inputRef.current.focus();
  }, []);

  return (
    <>
      <input
        type="text"
        placeholder="请输入用户名"
        ref={inputRef}  {/* 将 DOM 节点绑定到 ref */}
      />
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
    </>
  );
};

2.2 useRef 操作 DOM 的三步法

ini 复制代码
步骤一:声明 ref,初始值为 null
  const inputRef = useRef(null);
       │
       ▼
步骤二:在 JSX 中通过 ref 属性绑定
  <input ref={inputRef} />
       │
       ▼  React 挂载组件时,自动将 DOM 节点赋值给
       │  inputRef.current
       ▼
步骤三:在 useEffect 中使用
  useEffect(() => {
    inputRef.current.focus();  // current 指向 DOM 节点
  }, []);

2.3 为什么在 useEffect 中操作 DOM?

jsx 复制代码
// ❌ 错误:在渲染阶段直接操作
const App = () => {
  const inputRef = useRef(null);
  inputRef.current.focus();  // 此时 DOM 可能还没挂载!
  return <input ref={inputRef} />;
};

// ✅ 正确:在 useEffect 中操作
const App = () => {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();  // DOM 已挂载,安全操作
  }, []);
  return <input ref={inputRef} />;
};

渲染时序:

ini 复制代码
组件函数执行(渲染阶段)     DOM 还不存在
    │                          inputRef.current = null
    ▼
React 创建 DOM 节点           inputRef.current = <input>
    │
    ▼
useEffect 执行                此时可以安全操作 DOM
    │
    ▼
inputRef.current.focus()      ✅ DOM 已就绪

三、useRef 第二段位:可变对象(非响应式状态)

3.1 场景:计数器但不需要响应式

有时候你需要存储一个可变的值,但不希望它触发重新渲染

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

const App = () => {
  const numRef = useRef(0);           // 非响应式可变值
  const [, forceRender] = useState(0); // 响应式触发器

  console.log(numRef.current);

  return (
    <>
      <div
        onClick={() => {
          numRef.current += 1;  // 修改 ref 不会触发渲染
          forceRender();        // 手动触发渲染来更新 UI
        }}
      >
        {numRef.current}
      </div>
    </>
  );
};

3.2 useRef vs useState:响应式 vs 非响应式

ini 复制代码
useState(响应式):
  setCount(1) ──▶ React 检测到变化 ──▶ 重新渲染组件 ──▶ UI 更新
  ✅ 数据变化自动反映到 UI

useRef(非响应式):
  ref.current = 1 ──▶ React 不关心 ──▶ 不触发渲染 ──▶ UI 不变
  ✅ 数据变化但不触发渲染(性能优化)
维度 useState useRef
响应式 ✅ 修改触发重新渲染 ❌ 修改不触发渲染
用途 业务数据状态 DOM 引用、可变对象、Worker 等
渲染时保留 ✅ 每次渲染保留最新值 ✅ 每次渲染保留同一个对象
更新方式 setState(newValue) ref.current = newValue
触发渲染 ✅ 是 ❌ 否
适用场景 需要驱动 UI 更新的数据 不需要驱动 UI 的持久引用

3.3 什么时候用 useRef 存值?

jsx 复制代码
// 场景一:定时器 ID(不需要触发渲染)
const timerRef = useRef(null);
timerRef.current = setInterval(() => { ... }, 1000);
clearInterval(timerRef.current);

// 场景二:上一次的状态值(用于比较)
const prevCountRef = useRef(0);
const [count, setCount] = useState(0);
useEffect(() => {
  prevCountRef.current = count;  // 记录上一次的值
}, [count]);

// 场景三:是否是首次渲染的标记
const isFirstRender = useRef(true);
useEffect(() => {
  if (isFirstRender.current) {
    isFirstRender.current = false;
    return;  // 首次渲染跳过
  }
  // 非首次渲染的逻辑
}, [someValue]);

3.4 useRef 的核心定义

useRef 是 React 提供的一个持久可变对象 的 Hook 函数,经常用来引用 DOM 节点对象。它有一个 current 属性,可以指向任何值或对象,不会触发渲染

csharp 复制代码
useRef 返回的对象生命周期:

组件挂载 ──▶ 创建 ref 对象 { current: initialValue }
    │
    │  每次重新渲染:返回同一个对象(不会重新创建)
    │
组件更新 ──▶ ref.current 可以被修改,但不会触发渲染
    │
组件卸载 ──▶ ref 对象被销毁

四、JS 单线程与 Event Loop 的局限

4.1 为什么 JS 是单线程?

javascript 复制代码
// 如果 JS 是多线程:
// 线程 A:document.getElementById('app').innerHTML = 'Hello'
// 线程 B:document.getElementById('app').innerHTML = 'World'
// → 冲突!DOM 到底显示什么?

JS 设计为单线程的原因是保证一致性------浏览器中的交互(滚动、点击、输入)如果由多个线程同时操作 DOM,就会产生冲突。单线程保证了简单和安全。

4.2 Event Loop:单线程的异步魔法

vbnet 复制代码
┌──────────────────────────────────────────────┐
│              Event Loop 机制                   │
│                                              │
│  ┌─────────────────┐                         │
│  │   Call Stack     │  主线程执行栈            │
│  │  (同步代码)       │                         │
│  └────────┬────────┘                         │
│           │                                   │
│           ▼                                   │
│  ┌─────────────────┐    ┌─────────────────┐  │
│  │  Web APIs        │    │  Callback Queue │  │
│  │  (异步任务)       │    │  (回调队列)      │  │
│  │                  │    │                 │  │
│  │  setTimeout      │───▶│  回调函数排队    │  │
│  │  fetch           │───▶│  等待执行        │  │
│  │  DOM Events      │───▶│                 │  │
│  └─────────────────┘    └────────┬────────┘  │
│                                  │            │
│                          Call Stack 空闲时     │
│                          ◀──取出回调执行        │
└──────────────────────────────────────────────┘

Event Loop 的问题:

vbnet 复制代码
普通异步任务(fetch、setTimeout):
  ✅ 不阻塞主线程,Event Loop 能处理

耗时计算任务(大循环、LLM 推理、游戏物理引擎):
  ❌ 虽然是同步代码,但执行时间太长
  ❌ 占用主线程,阻塞所有交互
  ❌ 用户点击、滚动全部无响应
javascript 复制代码
// 这段代码会冻结页面 3 秒
console.time('主线程');
for (let i = 0; i < 100000000; i++) {
  // 耗时计算,主线程被完全阻塞
}
console.timeEnd('主线程');
// 在此期间,用户无法点击、滚动、输入

五、useRef 第三段位:管理 Web Worker

5.1 Web Worker:浏览器的多线程方案

scss 复制代码
┌──────────────────────────────────────────────────┐
│                 浏览器                             │
│                                                  │
│  ┌────────────────┐     ┌──────────────────┐    │
│  │   主线程        │     │   Worker 线程     │    │
│  │  (UI 渲染+交互) │     │  (复杂计算)       │    │
│  │                │     │                  │    │
│  │  React 渲染    │     │  LLM 推理        │    │
│  │  用户交互      │     │  游戏物理引擎     │    │
│  │  DOM 操作      │     │  大量数据处理     │    │
│  │                │     │                  │    │
│  │    postMessage │────▶│   onmessage     │    │
│  │                │◀────│   postMessage    │    │
│  └────────────────┘     └──────────────────┘    │
│                                                  │
│  独立内存空间,互不阻塞                            │
└──────────────────────────────────────────────────┘

Web Worker 的特点:

特性 主线程 Worker 线程
能操作 DOM ❌ 不能
能渲染 UI ❌ 不能
耗时计算 ❌ 会阻塞 ✅ 不阻塞主线程
内存空间 主线程内存 独立内存
通信方式 - postMessage 消息机制

5.2 用 useRef 持有 Worker 线程

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

function App() {
  // Worker 线程的引用,持久化存储
  const workerRef = useRef(null);

  useEffect(() => {
    // 开启一个 Worker 线程(开销较大,只创建一次)
    workerRef.current = new Worker(
      new URL('./worker.js', import.meta.url)
    );

    // 组件卸载时清理 Worker
    return () => {
      workerRef.current.terminate();
    };
  }, []);

  return (
    <>
      <button onClick={() => {
        // 向 Worker 发送消息
        workerRef.current.postMessage('start');
      }}>
        开始计算
      </button>
    </>
  );
}

export default App;

worker.js:

javascript 复制代码
// Worker 线程中的代码
console.log('worker online');

// 接收主线程消息
self.onmessage = function(event) {
  console.log('收到主线程消息:', event.data);

  // 执行耗时计算
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }

  // 将结果发送回主线程
  self.postMessage({ result });
};

5.3 为什么用 useRef 持有 Worker?

scss 复制代码
如果用 useState 存储 Worker:

const [worker, setWorker] = useState(null);
useEffect(() => {
  setWorker(new Worker(...));  // 触发重新渲染
}, []);

❌ 问题:
1. Worker 创建后不需要触发渲染
2. 每次 setWorker 都会导致组件重新渲染
3. Worker 对象不是"状态",是"引用"


如果用 useRef 存储 Worker:

const workerRef = useRef(null);
useEffect(() => {
  workerRef.current = new Worker(...);  // 不触发渲染
}, []);

✅ 优势:
1. 不触发不必要的渲染
2. Worker 对象在整个组件生命周期内持久存在
3. 符合 useRef 的语义------引用非响应式对象

5.4 Worker 创建语法解析

javascript 复制代码
workerRef.current = new Worker(
  new URL('./worker.js', import.meta.url)
);
arduino 复制代码
new URL('./worker.js', import.meta.url)
         │              │
         │              └── 基准路径:当前模块的 URL
         │
         └── 相对路径:Worker 脚本文件

最终解析为绝对路径:
  http://localhost:5173/src/worker.js (Vite 开发环境)
  https://example.com/assets/worker-abc123.js (生产构建)

new Worker(url)
  → 浏览器下载该脚本
  → 在独立线程中执行
  → 返回 Worker 实例

为什么用 new URL + import.meta.url

这是 Vite/Webpack 等构建工具推荐的方式。构建工具会识别这种写法,将 Worker 脚本单独打包成一个文件,而不是内联到主 bundle 中。

5.5 主线程与 Worker 的完整通信

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

function App() {
  const workerRef = useRef(null);
  const [result, setResult] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    workerRef.current = new Worker(
      new URL('./worker.js', import.meta.url)
    );

    // 监听 Worker 返回的消息
    workerRef.current.onmessage = function(event) {
      setResult(event.data.result);
      setLoading(false);
    };

    return () => {
      workerRef.current.terminate();
    };
  }, []);

  const handleCalculate = () => {
    setLoading(true);
    // 向 Worker 发送任务
    workerRef.current.postMessage({ task: 'calculate' });
  };

  return (
    <>
      <button onClick={handleCalculate} disabled={loading}>
        {loading ? '计算中...' : '开始计算'}
      </button>
      {result && <p>结果: {result}</p>}
    </>
  );
}

通信流程图:

scss 复制代码
主线程                          Worker 线程
   │                                │
   │  postMessage({task:'calc'})    │
   │───────────────────────────────▶│
   │                                │
   │                                │  执行耗时计算
   │                                │  (不阻塞主线程)
   │   用户可以正常交互              │
   │   页面滚动/点击正常              │
   │                                │
   │  postMessage({result: xxx})    │
   │◀───────────────────────────────│
   │                                │
   │  onmessage 接收结果             │
   │  setResult(xxx) 更新 UI         │
   ▼                                ▼

六、useRef 的三个段位总结

ini 复制代码
段位一:DOM 引用(最常用)
  ┌─────────────────────────────┐
  │ const inputRef = useRef(null) │
  │ <input ref={inputRef} />     │
  │ inputRef.current.focus()     │
  └─────────────────────────────┘
  场景:focus、滚动定位、测量元素尺寸、操作 canvas

段位二:可变对象(非响应式存储)
  ┌─────────────────────────────┐
  │ const timerRef = useRef(null) │
  │ timerRef.current = setInterval│
  │ clearInterval(timerRef.current)│
  └─────────────────────────────┘
  场景:定时器 ID、上一次的值、首次渲染标记

段位三:Worker 线程管理(高级)
  ┌─────────────────────────────┐
  │ const workerRef = useRef(null)│
  │ workerRef.current = new Worker│
  │ workerRef.current.postMessage │
  └─────────────────────────────┘
  场景:复杂计算、LLM 推理、游戏物理引擎

七、最佳实践

7.1 什么时候该用 useRef?

场景 用 useState 用 useRef
需要驱动 UI 更新的数据
DOM 节点引用
定时器/订阅 ID
Worker 实例
不需要触发渲染的中间值
跨渲染周期保留的引用

7.2 useRef 使用规范

jsx 复制代码
// ✅ 初始值要明确
const inputRef = useRef(null);       // DOM 引用,初始 null
const countRef = useRef(0);          // 数值,初始 0
const workerRef = useRef(null);      // Worker,初始 null

// ✅ 修改 current 属性,不要重新赋值整个 ref
ref.current = newValue;              // ✅
// ref = { current: newValue };      // ❌ 不要这样做

// ✅ 清理副作用
useEffect(() => {
  workerRef.current = new Worker(...);
  return () => {
    workerRef.current.terminate();   // 卸载时清理
  };
}, []);

// ❌ 不要在渲染阶段读写 ref.current
const App = () => {
  const ref = useRef(0);
  ref.current = 1;  // ❌ 渲染阶段不要修改 ref
  return <div />;
};

// ✅ 在事件处理或 useEffect 中修改
const App = () => {
  const ref = useRef(0);
  useEffect(() => {
    ref.current = 1;  // ✅ 在 useEffect 中修改
  }, []);
  return <div />;
};

八、总结

8.1 知识体系图

sql 复制代码
useRef(持久可变对象 Hook)
├── 段位一:DOM 引用
│   ├── useRef(null) → ref 属性绑定 → current 操作 DOM
│   └── 必须在 useEffect 中操作(等 DOM 挂载完成)
├── 段位二:非响应式可变值
│   ├── 不触发渲染的数据存储
│   ├── 定时器 ID、上一次的值、首次渲染标记
│   └── vs useState:响应式 vs 非响应式
└── 段位三:Worker 线程管理
    ├── JS 单线程 → Event Loop → 耗时任务阻塞
    ├── Web Worker 独立线程,postMessage 通信
    └── useRef 持有 Worker 实例,不触发渲染

8.2 核心概念速查

概念 要点
DOM 编程为什么慢 JS 引擎与渲染引擎跨引擎通信开销
React 规避 DOM useState 数据驱动,React 自动更新 DOM
useRef 定义 持久可变对象,有 current 属性,不触发渲染
useRef vs useState 非响应式 vs 响应式
JS 单线程 保证 DOM 操作一致性
Event Loop 局限 异步无阻塞,但耗时同步任务会卡死主线程
Web Worker 浏览器多线程方案,独立内存,postMessage 通信
useRef + Worker 用 ref 持有 Worker 实例,不触发渲染

8.3 React Hooks 的分工

复制代码
useState   → 响应式业务数据(触发渲染)
useEffect  → 副作用管理(请求、订阅、DOM 操作)
useRef     → 非响应式持久引用(DOM、Worker、可变值)

三个 Hook 各司其职:useState 管数据状态,useEffect 管副作用,useRef 管引用。理解它们的边界,才能写出高性能、可维护的 React 组件。


如果这篇文章对你有帮助,欢迎点赞收藏

相关推荐
张元清2 小时前
React useEvent Hook:告别过期闭包的稳定回调 (2026)
javascript·react.js
用户938515635072 小时前
从 DOM 编程到声明式 UI:React useRef 与 useState 底层全解析
前端·javascript·react.js
kisshyshy3 小时前
前端路由进化史:从刷新白屏到SPA,手写一个Hash路由就懂了!
前端·javascript·react.js
名字还没想好☜4 小时前
React 用 Portal + Context 实现全局 Toast:一次调用、自动消失与队列管理
前端·javascript·react.js·react·next.js
sugar__salt4 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
用户2930750976695 小时前
React Hooks 与前端编程模型:从 DOM 操作到 Web Worker
react.js
无人生还5 小时前
从 Vue3 到 React · 快速上手系列第 13 篇:工程化与综合实战(Vite + Todo 应用)
前端·vue.js·react.js
breeze jiang6 小时前
React useRef 实战:从 input 聚焦到 Web Worker 引用
前端·javascript·react.js
Rysxt_8 小时前
React vs Vue.js:2025年两大主流前端框架深度对比教程
vue.js·react.js·前端框架