当前位置: 首页 > 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.zskr.cn/news/1490918.html

相关文章:

  • Calibre Image Actions技术深度解析:基于libvips的自动化图片压缩解决方案
  • 手把手教你配置锐捷AC的BFD链路:保障VAC高可用的关键一步
  • WaxPatch高级应用:实现复杂UI动态修改与业务逻辑热更新
  • 告别裸机:在FreeRTOS上为STM32移植SOEM 1.4.0的完整指南
  • 用Cheat Engine给植物大战僵尸“动手术”:从阳光到僵尸血量的完整逆向实战(附C++代码)
  • 告别信息孤岛:如何用OPC UA和Euromap 63协议打通注塑机与MES/云平台
  • MuleSoft AI编排实战:企业级LLM集成的架构设计与故障治理
  • MediaPipe人脸检测Python调用包:含关键点定位、边界框识别与姿态估计
  • 架构级Windows系统性能调优:AtlasOS深度解析与实战指南
  • Python语音合成实战:从文本清洗到树莓派部署
  • DVWA靶场实战:手把手教你用XSS平台盗取Cookie并登录后台(保姆级避坑指南)
  • Anthropic新API层归零:/v1/messages如何重构AI工程范式
  • GD32F303片内FLASH读写避坑指南:从EEPROM到FLASH,你的数据存储姿势对了吗?
  • 纯前端网页文件预览工具:本地打开即用,支持PDF/Office/图片在线查看
  • 你的第一个量化分析项目:从用efinance获取茅台股票数据开始
  • 别再让神经网络‘猜平均’了:用PyTorch实现MDN搞定‘一对多’预测难题
  • Proteus仿真DS18B20温控器,从驱动到逻辑控制保姆级代码解析
  • 别再乱接线了!手把手教你用USB转TTL模块正确配置HC-05蓝牙(附AT指令详解)
  • 告别打印失败!OrcaSlicer-bambulab的智能支撑生成与优化技巧全解析
  • 8K上下文窗口!Fox-1-1.6B-Instruct-v0.1长文本处理能力实测指南
  • LLM数据生命周期防护:面向大模型的动态DLP实践指南
  • 02-Hooks完全指南——03-useContext 与跨组件通信
  • HarmonyOS 手写笔服务:让你的应用支持手写输入
  • AMD Ryzen调试终极指南:5分钟掌握SMU Debug Tool完整教程
  • 济南千鸿黄金回收市中区门店 - 润富黄金回收
  • 从多普勒效应到代码:深入理解无线通信中的频率偏移与同步(以QPSK/16QAM为例)
  • 大模型评估体系全解:如何科学衡量你的 LLM 应用质量?
  • 如何用Dify工作流模板快速构建专业级AI应用?实战方法揭秘
  • 全程用 AI 做一款商业级手游 · EP9 收尾与复盘:做到了哪,没做到哪,边界在哪
  • 2026年加固笔记本电脑应用白皮书智能制造领域解析:防爆计算机/三防电脑/便携式加固计算机/实力盘点 - 优质品牌商家