欢迎光临
我们一直在努力

React useRef 完全指南:从入门到实战

一句话总结: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:傻傻分不清?🤔

这是新手最容易混淆的地方,让我们用一张表说清楚:

特性useState 🔄useRef 📦
修改后会重新渲染? ✅ 会! ❌ 不会!
适合存什么? 需要展示在界面上的数据 不需要展示的数据、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 的魔力!

赞(0)
未经允许不得转载:171主机测评 » React useRef 完全指南:从入门到实战
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址