news 2026/6/21 6:22:25

02-Hooks完全指南——04-useRef 与 DOM 操作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
02-Hooks完全指南——04-useRef 与 DOM 操作

useRef 与 DOM 操作

一、useRef 基础

1.1 基本语法

const refContainer = useRef(initialValue);
  • refContainer.current:存储可变值
  • initialValue:初始值
  • 修改ref.current不会触发组件重新渲染

1.2 useRef 的两种主要用途

用途说明示例
DOM 引用直接访问 DOM 元素聚焦输入框、获取尺寸
可变值存储存储跨渲染周期的值定时器 ID、上一次的值

二、DOM 引用

2.1 基础 DOM 操作

function InputFocus() { const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); }; return ( <div> <input ref={inputRef} type="text" placeholder="点击按钮聚焦" /> <button onClick={focusInput}>聚焦输入框</button> </div> ); }

2.2 获取元素属性

function MeasureElement() { const divRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const measure = () => { if (divRef.current) { const { width, height } = divRef.current.getBoundingClientRect(); setDimensions({ width, height }); } }; useEffect(() => { measure(); window.addEventListener('resize', measure); return () => window.removeEventListener('resize', measure); }, []); return ( <div> <div ref={divRef} style={{ width: '50%', padding: '20px', background: '#f0f0f0', resize: 'both', overflow: 'auto' }} > 宽度: {Math.round(dimensions.width)}px<br /> 高度: {Math.round(dimensions.height)}px </div> </div> ); }

2.3 操作多个元素

function MultipleRefs() { const inputRefs = useRef([]); const focusNext = (index) => { if (index < inputRefs.current.length - 1) { inputRefs.current[index + 1].focus(); } }; const handleKeyDown = (e, index) => { if (e.key === 'Enter') { focusNext(index); } }; return ( <div> {[0, 1, 2, 3].map((_, index) => ( <input key={index} ref={el => inputRefs.current[index] = el} onKeyDown={(e) => handleKeyDown(e, index)} placeholder={`输入框 ${index + 1}`} /> ))} </div> ); }

三、存储可变值

3.1 存储定时器 ID

function Timer() { const [count, setCount] = useState(0); const intervalRef = useRef(null); const startTimer = () => { if (intervalRef.current) return; intervalRef.current = setInterval(() => { setCount(prev => prev + 1); }, 1000); }; const stopTimer = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; useEffect(() => { return () => stopTimer(); // 清理 }, []); return ( <div> <p>计数: {count}</p> <button onClick={startTimer}>开始</button> <button onClick={stopTimer}>停止</button> <button onClick={() => setCount(0)}>重置</button> </div> ); }

3.2 存储上一次的值

function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return ( <div> <p>当前: {count}</p> <p>上一次: {prevCount}</p> <button onClick={() => setCount(count + 1)}>增加</button> </div> ); }

3.3 避免闭包陷阱

function ClosureSolution() { const [count, setCount] = useState(0); const countRef = useRef(count); // 同步最新值到 ref useEffect(() => { countRef.current = count; }, [count]); useEffect(() => { const timer = setInterval(() => { // 使用 ref 获取最新值,避免闭包 console.log('当前 count:', countRef.current); }, 1000); return () => clearInterval(timer); }, []); return <button onClick={() => setCount(count + 1)}>增加: {count}</button>; }

四、与第三方库集成

4.1 集成 Swiper

import Swiper from 'swiper'; function SwiperComponent() { const swiperRef = useRef(null); const containerRef = useRef(null); useEffect(() => { swiperRef.current = new Swiper(containerRef.current, { slidesPerView: 1, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' } }); return () => { if (swiperRef.current) { swiperRef.current.destroy(); } }; }, []); return ( <div ref={containerRef} className="swiper-container"> <div className="swiper-wrapper"> <div className="swiper-slide">Slide 1</div> <div className="swiper-slide">Slide 2</div> <div className="swiper-slide">Slide 3</div> </div> <div className="swiper-button-prev"></div> <div className="swiper-button-next"></div> </div> ); }

4.2 集成 Chart.js

import Chart from 'chart.js/auto'; function ChartComponent({ data, type = 'line' }) { const canvasRef = useRef(null); const chartRef = useRef(null); useEffect(() => { if (chartRef.current) { chartRef.current.destroy(); } chartRef.current = new Chart(canvasRef.current, { type, data, options: { responsive: true, maintainAspectRatio: false } }); return () => { if (chartRef.current) { chartRef.current.destroy(); } }; }, [data, type]); return <canvas ref={canvasRef} style={{ width: '100%', height: '400px' }} />; }

4.3 集成第三方输入库

import Cleave from 'cleave.js'; function PhoneInput() { const inputRef = useRef(null); const cleaveRef = useRef(null); useEffect(() => { cleaveRef.current = new Cleave(inputRef.current, { phone: true, phoneRegionCode: 'CN' }); return () => { if (cleaveRef.current) { cleaveRef.current.destroy(); } }; }, []); return <input ref={inputRef} placeholder="手机号码" />; }

五、性能优化

5.1 使用回调 Ref 代替 useRef

function CallbackRef() { const [height, setHeight] = useState(0); // 回调 ref 可以在节点挂载/卸载时获得通知 const measureRef = useCallback((node) => { if (node) { setHeight(node.getBoundingClientRect().height); } }, []); return ( <div> <div ref={measureRef} style={{ padding: '20px' }}> 测量我的高度 </div> <p>高度: {height}px</p> </div> ); }

5.2 避免不必要的 ref 更新

function OptimizedRef() { const ref = useRef(null); // 使用 useCallback 避免重新创建回调 const setRef = useCallback((node) => { if (node) { // 只在节点挂载时执行 node.style.opacity = 1; } ref.current = node; }, []); return <div ref={setRef}>内容</div>; }

六、常见陷阱

6.1 Ref 更新不触发渲染

function RefDoesNotRender() { const [renderCount, setRenderCount] = useState(0); const refValue = useRef(0); const updateRef = () => { refValue.current++; console.log('ref 值:', refValue.current); // 不会触发重新渲染,UI 不更新 }; const updateState = () => { setRenderCount(prev => prev + 1); // 触发重新渲染 }; return ( <div> <p>Ref 值: {refValue.current}(不会自动更新 UI)</p> <p>渲染次数: {renderCount}</p> <button onClick={updateRef}>更新 Ref</button> <button onClick={updateState}>更新 State</button> </div> ); }

6.2 条件渲染中的 Ref

function ConditionalRef() { const [show, setShow] = useState(false); const inputRef = useRef(null); const focusInput = () => { if (inputRef.current) { inputRef.current.focus(); } }; return ( <div> <button onClick={() => setShow(!show)}>切换</button> <button onClick={focusInput}>聚焦</button> {show && <input ref={inputRef} placeholder="条件渲染的输入框" />} </div> ); }

6.3 在渲染期间访问 Ref

function BadExample() { const ref = useRef(null); // ❌ 错误:在渲染期间访问 ref if (ref.current) { console.log(ref.current.value); // 可能未定义或过时 } // ✅ 正确:在 useEffect 或事件中访问 useEffect(() => { if (ref.current) { console.log(ref.current.value); } }, []); return <input ref={ref} />; }

七、useRef vs useState

特性useRefuseState
更新触发渲染❌ 否✅ 是
值持久化✅ 是✅ 是
异步更新同步异步
适用场景DOM 操作、存储值UI 状态
function Comparison() { const refCount = useRef(0); const [stateCount, setStateCount] = useState(0); const incrementRef = () => { refCount.current++; console.log('ref:', refCount.current); // 立即更新 }; const incrementState = () => { setStateCount(stateCount + 1); console.log('state:', stateCount); // 还是旧值 }; return ( <div> <p>Ref: {refCount.current}</p> <p>State: {stateCount}</p> <button onClick={incrementRef}>Ref +1</button> <button onClick={incrementState}>State +1</button> </div> ); }

八、练习题

基础题

  1. 实现一个表单,提交后自动聚焦到第一个输入框
  2. 实现一个视频播放器,支持播放/暂停

进阶题

  1. 实现一个可拖拽的对话框
  2. 实现一个无限滚动列表,使用 ref 检测滚动到底部

参考答案

// 1. 可拖拽对话框 function DraggableDialog() { const [position, setPosition] = useState({ x: 100, y: 100 }); const [isDragging, setIsDragging] = useState(false); const dragRef = useRef({ startX: 0, startY: 0 }); const handleMouseDown = (e) => { setIsDragging(true); dragRef.current = { startX: e.clientX - position.x, startY: e.clientY - position.y }; }; const handleMouseMove = (e) => { if (!isDragging) return; setPosition({ x: e.clientX - dragRef.current.startX, y: e.clientY - dragRef.current.startY }); }; const handleMouseUp = () => { setIsDragging(false); }; useEffect(() => { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging]); return ( <div style={{ position: 'fixed', left: position.x, top: position.y, width: '300px', background: 'white', border: '1px solid #ccc', cursor: isDragging ? 'grabbing' : 'grab' }} > <div onMouseDown={handleMouseDown} style={{ padding: '10px', background: '#007bff', color: 'white', cursor: 'grab' }} > 拖拽标题栏 </div> <div style={{ padding: '20px' }}>可拖拽对话框内容</div> </div> ); }

九、小结

要点说明
DOM 操作使用 ref 获取 DOM 元素引用
存储值存储不触发渲染的可变值
性能回调 ref 可在节点变化时通知
清理记得清理第三方库实例

核心要点:

  • useRef 不触发重新渲染
  • 用于 DOM 操作和存储可变值
  • 回调 ref 可获取节点挂载/卸载通知
  • 与第三方库集成时记得清理

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 22:18:35

【Springboot毕设全套源码+文档】基于java的养生药膳食疗系统的设计与实现(丰富项目+远程调试+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/6/9 5:04:19

B站视频下载终极指南:用BBDown轻松保存你喜欢的视频内容

B站视频下载终极指南&#xff1a;用BBDown轻松保存你喜欢的视频内容 【免费下载链接】BBDown Bilibili Downloader. 一个命令行式哔哩哔哩下载器. 项目地址: https://gitcode.com/gh_mirrors/bb/BBDown 你是不是经常在B站看到精彩的教学视频、有趣的Vlog或重要的学习资料…

作者头像 李华
网站建设 2026/6/11 10:44:29

PySpark多字符分隔符解析实战:从UDF到multiCharSep的四套生产方案

1. 项目概述&#xff1a;为什么多字符分隔符在真实数据场景中是个“隐形炸弹”PySpark处理多字符分隔符数据集——这听起来像一句技术文档里的标准描述&#xff0c;但在我过去三年带团队落地的17个金融、电商和日志分析项目里&#xff0c;它几乎每次都是ETL pipeline崩溃的第一…

作者头像 李华
网站建设 2026/6/9 4:58:55

SeetaFaceEngine2 Android开发实战:移动端人脸识别应用开发指南

SeetaFaceEngine2 Android开发实战&#xff1a;移动端人脸识别应用开发指南 【免费下载链接】SeetaFaceEngine2 项目地址: https://gitcode.com/gh_mirrors/se/SeetaFaceEngine2 SeetaFaceEngine2是一款功能强大的开源人脸识别引擎&#xff0c;专为移动端优化&#xff…

作者头像 李华
网站建设 2026/6/11 6:58:48

工业级机器学习工程化:特征管理、模型服务与漂移监控实战

1. 项目概述&#xff1a;这不是“黑科技”&#xff0c;而是被低估的工程化红利“This ML Project Gives You an Unfair Advantage”——这个标题乍看像营销话术&#xff0c;但在我带过27个工业级AI落地项目、亲手调过14万组超参、在金融风控、智能客服、供应链预测三条主线上反…

作者头像 李华