三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

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

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

← 返回列表