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

01-React基础入门——11-Refs 与 DOM 操作

Refs 与 DOM 操作

一、Refs 概述

1.1 什么是 Refs?

Refs 是 React 提供的"逃生舱",用于直接访问 DOM 元素或组件实例。在大多数情况下应该使用声明式的 React 数据流,但有些场景必须直接操作 DOM。

1.2 何时使用 Refs

场景是否推荐使用 Refs
管理焦点、文本选择✅ 推荐
媒体播放控制(video/audio)✅ 推荐
触发强制动画✅ 推荐
集成第三方 DOM 库✅ 推荐
获取元素尺寸位置✅ 推荐
通过 props 传递数据❌ 应使用 state
触发状态更新❌ 应使用 state

二、创建和使用 Refs

2.1 useRef Hook(函数组件)

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

2.2 createRef(类组件)

class TextInput extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); } focusInput = () => { this.inputRef.current.focus(); }; render() { return ( <div> <input ref={this.inputRef} type="text" /> <button onClick={this.focusInput}>聚焦</button> </div> ); } }

2.3 回调 Refs

function CallbackRef() { const [height, setHeight] = useState(0); const measureRef = useCallback((node) => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []); return ( <div ref={measureRef}> 我的高度是: {height}px </div> ); }

三、Refs 常见操作

3.1 管理焦点

function AutoFocusForm() { const nameRef = useRef(null); const emailRef = useRef(null); const passwordRef = useRef(null); const handleKeyDown = (e, nextRef) => { if (e.key === 'Enter') { nextRef.current.focus(); } }; useEffect(() => { nameRef.current.focus(); // 自动聚焦第一个输入框 }, []); return ( <form> <input ref={nameRef} onKeyDown={(e) => handleKeyDown(e, emailRef)} placeholder="姓名" /> <input ref={emailRef} onKeyDown={(e) => handleKeyDown(e, passwordRef)} placeholder="邮箱" /> <input ref={passwordRef} type="password" placeholder="密码" /> </form> ); }

3.2 媒体控制

function VideoPlayer({ src }) { const videoRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const togglePlay = () => { if (isPlaying) { videoRef.current.pause(); } else { videoRef.current.play(); } setIsPlaying(!isPlaying); }; return ( <div> <video ref={videoRef} src={src} width="100%" controls /> <button onClick={togglePlay}>{isPlaying ? '暂停' : '播放'}</button> </div> ); }

3.3 获取元素尺寸

function MeasureElement() { const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const elementRef = useRef(null); useEffect(() => { const updateDimensions = () => { if (elementRef.current) { const { width, height } = elementRef.current.getBoundingClientRect(); setDimensions({ width, height }); } }; updateDimensions(); window.addEventListener('resize', updateDimensions); return () => window.removeEventListener('resize', updateDimensions); }, []); return ( <div> <div ref={elementRef} style={{ width: '50%', padding: '20px', background: '#f0f0f0' }} > 尺寸: {dimensions.width}px x {dimensions.height}px </div> </div> ); }

3.4 滚动控制

function ScrollController() { const listRef = useRef(null); const scrollToBottom = () => { listRef.current.scrollTop = listRef.current.scrollHeight; }; const scrollToTop = () => { listRef.current.scrollTop = 0; }; return ( <div> <div ref={listRef} style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }} > {Array.from({ length: 50 }, (_, i) => ( <div key={i}>第 {i + 1} 项</div> ))} </div> <button onClick={scrollToTop}>滚动到顶部</button> <button onClick={scrollToBottom}>滚动到底部</button> </div> ); }

3.5 存储可变值

function TimerWithRef() { const [count, setCount] = useState(0); const intervalRef = useRef(null); const startTimer = () => { if (intervalRef.current) return; intervalRef.current = setInterval(() => { setCount(c => c + 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> ); }

四、转发 Refs (forwardRef)

4.1 基础转发

// 子组件 const FancyInput = forwardRef((props, ref) => { return <input ref={ref} className="fancy-input" {...props} />; }); // 父组件 function Parent() { const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); }; return ( <div> <FancyInput ref={inputRef} placeholder="自定义输入框" /> <button onClick={focusInput}>聚焦</button> </div> ); }

4.2 转发多个 Refs

const FormInput = forwardRef((props, ref) => { const inputRef = useRef(null); // 暴露多个方法 useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), blur: () => inputRef.current.blur(), getValue: () => inputRef.current.value, setValue: (value) => { inputRef.current.value = value; } })); return <input ref={inputRef} {...props} />; }); function Parent() { const formRef = useRef(null); return ( <div> <FormInput ref={formRef} placeholder="输入内容" /> <button onClick={() => formRef.current.focus()}>聚焦</button> <button onClick={() => console.log(formRef.current.getValue())}> 获取值 </button> </div> ); }

4.3 useImperativeHandle

const CustomInput = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, shake: () => { inputRef.current.style.transform = 'translateX(5px)'; setTimeout(() => { inputRef.current.style.transform = 'translateX(0)'; }, 100); }, clear: () => { inputRef.current.value = ''; } })); return <input ref={inputRef} {...props} />; });

五、第三方库集成

5.1 集成 Swiper

import Swiper from 'swiper'; function SwiperComponent() { const swiperRef = useRef(null); useEffect(() => { swiperRef.current = new Swiper('.swiper-container', { slidesPerView: 1, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' } }); return () => { swiperRef.current.destroy(); }; }, []); return ( <div 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> ); }

5.2 集成 Chart.js

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

六、常见陷阱与注意事项

6.1 Refs 更新时机

function RefTiming() { const ref = useRef(0); const [state, setState] = useState(0); const updateRef = () => { ref.current = ref.current + 1; console.log('ref 已更新:', ref.current); // 不会触发重新渲染 }; const updateState = () => { setState(state + 1); // 触发重新渲染 }; return ( <div> <p>Ref 值: {ref.current}(不会在 UI 更新)</p> <p>State 值: {state}</p> <button onClick={updateRef}>更新 Ref</button> <button onClick={updateState}>更新 State</button> </div> ); }

6.2 条件渲染中的 Refs

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 不要在渲染期间读取 Refs

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} />; }

七、性能优化

7.1 避免不必要的 Refs 更新

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

7.2 使用 ResizeObserver

function ResizeObserverExample() { const elementRef = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { if (!elementRef.current) return; const observer = new ResizeObserver(entries => { const { width, height } = entries[0].contentRect; setSize({ width, height }); }); observer.observe(elementRef.current); return () => observer.disconnect(); }, []); return ( <div ref={elementRef} style={{ resize: 'both', overflow: 'auto', border: '1px solid #ccc' }}> 宽度: {Math.round(size.width)}px, 高度: {Math.round(size.height)}px </div> ); }

八、练习题

基础题

  1. 实现一个 Todo 列表,添加新项后自动滚动到底部
  2. 实现一个表单,提交后自动清空并聚焦到第一个输入框

进阶题

  1. 实现一个图片懒加载组件
  2. 实现一个可拖拽排序的列表

参考答案

// 1. 自动滚动列表 function AutoScrollList() { const [items, setItems] = useState([]); const [input, setInput] = useState(''); const listRef = useRef(null); const bottomRef = useRef(null); const addItem = () => { if (input.trim()) { setItems([...items, input]); setInput(''); } }; useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [items]); return ( <div> <div ref={listRef} style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}> {items.map((item, index) => ( <div key={index}>{item}</div> ))} <div ref={bottomRef} /> </div> <input value={input} onChange={(e) => setInput(e.target.value)} /> <button onClick={addItem}>添加</button> </div> ); }

九、小结

要点说明
useRef函数组件中创建 ref
createRef类组件中创建 ref
forwardRef转发 ref 到子组件
useImperativeHandle自定义暴露给父组件的实例值
回调 ref更精细地控制 ref 设置

核心要点

  • Refs 是操作 DOM 的逃生舱
  • 优先使用声明式方案
  • 使用 forwardRef 传递 ref
  • 注意 ref 不会触发重新渲染
  • 在 useEffect 或事件中访问 ref

http://www.zskr.cn/news/1471035.html

相关文章:

  • 【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
  • 2026年质量好的镶件机械手/车床机械手/伺服机械手深度厂家推荐 - 品牌宣传支持者
  • 2026年口碑好的地库地坪/无机磨石地坪/混凝土地面施工/厂房地坪生产厂家推荐 - 行业平台推荐
  • 从STM32转战HC32,GPIO配置这5个坑我帮你踩过了(含解锁、等待时间、复用功能避坑)
  • GRB X射线吸收研究:TEPID模型与介质特性分析
  • 入门大模型工程师第五课----通过微调改善大模型在垂直领域的表现
  • 告别接线混乱!ESP8266驱动1.44寸ST7735屏,TFT_eSPI库的OVERLAP模式实战(附完整代码)
  • 告别原生File类:用Hutool的FileUtil,5分钟搞定Java文件操作(附避坑指南)
  • VS Code + Cursor + Continue + Warp + LangChain + Ollama —— 这套组合为何让资深工程师日均编码时长缩短2.8小时?
  • 基于小程序的医疗报销系统的设计与实现毕业设计源码
  • STM32CubeMX配置USART空闲中断+DMA接收不定长数据,5分钟搞定(HAL库版)
  • Speechless终极指南:3分钟学会微博备份,永久保存你的数字记忆
  • 别再迷信软件了!用Python自己算筹码获利比(Winner函数),避免数据黑箱
  • 2026年热门的双臂机械手/三轴机械手推荐品牌厂家 - 行业平台推荐
  • 别再让同事乱Push了!手把手教你用GitLab分支保护,把CodeReview做在合并前
  • UDS服务0x19到底做了什么?为什么一个ReadDTCInformation请求能把DEM全部串起来?
  • 从零到一:手把手教你用Python复现GNSS-RTK/INS紧组合算法(附开源项目IGNAV实战)
  • 拓扑数据分析在天体物理预测中的应用
  • Cesium for Unity终极指南:5分钟创建真实世界3D场景
  • 宝塔面板一键部署的PHP自助建站源码,含多模板+自定义支付功能
  • 数据埋点与留存分析:核心链路的 DAU 观测实战
  • PHPShell脚本与系统命令调用
  • 别再一张张修图了!Photoshop Camera RAW 批量同步调色,5分钟搞定一组风光照
  • 告别打印烦恼:手把手教你用JavaScript在Web端驱动斑马打印机打印二维码(附ZD888/GT800通用代码)
  • 告别中间商!Foobar2000直通ASIO+DSD硬解保姆级教程(附插件下载)
  • 2026年6月市场优质的市场调研公司推荐,神秘顾客/门店暗访/市场调研/门店检查/广告监测,市场调研机构哪个好 - 品牌推荐师
  • 别再只会画流程图了!Flowable流程设计器里任务监听器和多实例的实战用法详解
  • 2026年靠谱的豪宅设计与装修公司/工厂装修公司/高端别墅设计与装修公司/商业空间装修公司哪家环保好 - 品牌宣传支持者
  • Qt项目实战:给你的软件加个‘优雅等待’功能,从原理到封装一网打尽
  • 宝塔面板下PHP8.0安装Swoole扩展,手把手教你搞定WebSocket实时通讯服务