当前位置: 首页 > news >正文

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 可获取节点挂载/卸载通知
  • 与第三方库集成时记得清理

http://www.jsqmd.com/news/979194/

相关文章:

  • Pandas多维聚合实战:银行级生产环境避坑指南
  • Calibre Image Actions技术深度解析:基于libvips的自动化图片压缩解决方案
  • 基于Hadoop的招聘数据全流程分析系统(Java实现,含Web界面与完整部署脚本)
  • PDF与CDF在机器学习中的工程实战:从概率校准到动态阈值
  • JavaScript面试宝典front-end-interview-questions:从初级到高级的50+核心问题
  • Openpyxl样式避坑指南:解决字体不生效、边框显示异常等5个常见问题
  • 构建AI个人导师:结构化教练协议设计与落地
  • 重庆社区小面技术拆解:从食材到运营的硬核标准 - 优质品牌商家
  • 你的量化策略缺数据?试试这个免费的efinance库,股票债券期货数据一键打包
  • 别再只靠GUI了!用APDL命令流高效管理你的ANSYS分析项目
  • 跟我一起学“仓颉”设计模式-桥接模式
  • 告别裸机:在FreeRTOS上为STM32移植SOEM 1.4.0的完整指南
  • WaxPatch高级应用:实现复杂UI动态修改与业务逻辑热更新
  • 手把手教你配置锐捷AC的BFD链路:保障VAC高可用的关键一步
  • 肥胖数据分析实战:从BMI计算到腰围-种族交互效应的公共卫生建模
  • 【江门六大黄金回收门店横向评测 附避坑指南】 - 润富黄金回收
  • MuleSoft AI编排实战:企业级LLM集成的架构设计与故障治理
  • Horizon Agent在RDS服务器上的安装与应用程序池发布指南(2111.1版本)
  • 用Cheat Engine给植物大战僵尸“动手术”:从阳光到僵尸血量的完整逆向实战(附C++代码)
  • 告别信息孤岛:如何用OPC UA和Euromap 63协议打通注塑机与MES/云平台
  • MyBatis-Plus 多租户实战
  • AI殖民协议:领地权、资源税与主权退出的多智能体自治设计
  • 网盘直链下载助手:打破下载限制的九大网盘通用解决方案
  • 告别Altera EPM240T100C5N?手把手教你用AG256SL100实现国产CPLD平替(附引脚兼容对照表)
  • MediaPipe人脸检测Python调用包:含关键点定位、边界框识别与姿态估计
  • 告别虚拟机卡顿:实测在Windows 11上用WSL2搭建Matter开发环境(附完整避坑清单)
  • Python语音合成实战:从文本清洗到树莓派部署
  • 架构级Windows系统性能调优:AtlasOS深度解析与实战指南
  • TinyML工程实践:面向嵌入式设备的端侧机器学习落地指南
  • 第【11】期--基于智能反射面的MIMO安全速率最大化研究-maltab完整代码+完整报告