Ref与DOM操作 #
一、Ref概述 #
1.1 什么是Ref #
Ref(Reference)提供了一种访问 DOM 节点或 React 组件实例的方式。
1.2 Ref使用场景 #
| 场景 | 说明 |
|---|---|
| 焦点管理 | 自动聚焦输入框 |
| 文本选择 | 选择输入框内容 |
| 媒体播放 | 控制视频/音频 |
| 动画触发 | 触发CSS动画 |
| 第三方库 | 集成非React库 |
| 测量DOM | 获取元素尺寸位置 |
1.3 何时使用Ref #
javascript
// ✅ 适合使用Ref的场景
- 焦点管理、文本选择
- 触发动画
- 集成第三方DOM库
- 计时器ID存储
// ❌ 不应该使用Ref的场景
- 表单验证(应该用state)
- 条件渲染(应该用state)
- 列表渲染(应该用state)
二、创建Ref #
2.1 useRef Hook #
javascript
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦</button>
</div>
);
}
2.2 createRef(类组件) #
javascript
import { Component, createRef } from 'react';
class TextInput extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
}
focusInput = () => {
this.inputRef.current.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} type="text" />
<button onClick={this.focusInput}>聚焦</button>
</div>
);
}
}
2.3 回调Ref #
javascript
function TextInput() {
let inputElement = null;
const setInputRef = (element) => {
inputElement = element;
};
const focusInput = () => {
if (inputElement) {
inputElement.focus();
}
};
return (
<div>
<input ref={setInputRef} type="text" />
<button onClick={focusInput}>聚焦</button>
</div>
);
}
2.4 useRef vs createRef #
| 特性 | useRef | createRef |
|---|---|---|
| 适用组件 | 函数组件 | 类组件 |
| 持久性 | 组件生命周期内不变 | 每次渲染创建新实例 |
| 推荐程度 | 推荐 | 类组件使用 |
三、访问DOM #
3.1 基本DOM操作 #
javascript
function InputDemo() {
const inputRef = useRef(null);
const focus = () => {
inputRef.current.focus();
};
const clear = () => {
inputRef.current.value = '';
};
const select = () => {
inputRef.current.select();
};
return (
<div>
<input ref={inputRef} defaultValue="Hello" />
<button onClick={focus}>聚焦</button>
<button onClick={clear}>清空</button>
<button onClick={select}>选择</button>
</div>
);
}
3.2 获取元素尺寸 #
javascript
function MeasureDemo() {
const boxRef = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
const measure = () => {
if (boxRef.current) {
const { offsetWidth, offsetHeight } = boxRef.current;
setSize({ width: offsetWidth, height: offsetHeight });
}
};
return (
<div>
<div
ref={boxRef}
style={{ width: '200px', height: '100px', background: '#f0f0f0' }}
>
Box
</div>
<button onClick={measure}>测量</button>
<p>宽度: {size.width}, 高度: {size.height}</p>
</div>
);
}
3.3 滚动控制 #
javascript
function ScrollDemo() {
const containerRef = useRef(null);
const scrollToTop = () => {
containerRef.current.scrollTop = 0;
};
const scrollToBottom = () => {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
};
return (
<div>
<div
ref={containerRef}
style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}
>
{Array.from({ length: 50 }, (_, i) => (
<div key={i}>Item {i + 1}</div>
))}
</div>
<button onClick={scrollToTop}>滚动到顶部</button>
<button onClick={scrollToBottom}>滚动到底部</button>
</div>
);
}
3.4 媒体控制 #
javascript
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const play = () => {
videoRef.current.play();
};
const pause = () => {
videoRef.current.pause();
};
const setVolume = (volume) => {
videoRef.current.volume = volume;
};
return (
<div>
<video ref={videoRef} src={src} width="400" />
<div>
<button onClick={play}>播放</button>
<button onClick={pause}>暂停</button>
<input
type="range"
min="0"
max="1"
step="0.1"
onChange={(e) => setVolume(parseFloat(e.target.value))}
/>
</div>
</div>
);
}
四、useImperativeHandle #
4.1 基本用法 #
useImperativeHandle 可以自定义暴露给父组件的实例值。
javascript
import { useRef, useImperativeHandle, forwardRef } from 'react';
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
function App() {
const inputRef = useRef();
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<button onClick={() => inputRef.current.clear()}>清空</button>
</div>
);
}
4.2 配合useCallback优化 #
javascript
const FancyInput = forwardRef(function FancyInput({ defaultValue }, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
getValue: () => {
return inputRef.current.value;
},
setValue: (value) => {
inputRef.current.value = value;
},
reset: () => {
inputRef.current.value = defaultValue || '';
}
}), [defaultValue]);
return <input ref={inputRef} defaultValue={defaultValue} />;
});
五、forwardRef #
5.1 转发Ref #
javascript
import { forwardRef } from 'react';
const Button = forwardRef(function Button({ children, ...props }, ref) {
return (
<button ref={ref} {...props}>
{children}
</button>
);
});
function App() {
const buttonRef = useRef();
return (
<div>
<Button ref={buttonRef} onClick={() => console.log('clicked')}>
Click me
</Button>
<button onClick={() => buttonRef.current.focus()}>
聚焦按钮
</button>
</div>
);
}
5.2 高阶组件转发Ref #
javascript
function withLogging(Component) {
const WithLogging = forwardRef(function WithLogging(props, ref) {
useEffect(() => {
console.log('Component mounted/updated');
});
return <Component {...props} ref={ref} />;
});
WithLogging.displayName = `withLogging(${Component.displayName || Component.name})`;
return WithLogging;
}
六、Ref存储可变值 #
6.1 存储任意值 #
javascript
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
return (
<div>
<p>{seconds}秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
);
}
6.2 获取前一次值 #
javascript
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>
);
}
6.3 解决闭包问题 #
javascript
function Stopwatch() {
const [time, setTime] = useState(0);
const timeRef = useRef(time);
timeRef.current = time;
useEffect(() => {
const timer = setInterval(() => {
console.log('当前时间:', timeRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<p>{time}</p>
<button onClick={() => setTime(t => t + 1)}>增加</button>
</div>
);
}
七、最佳实践 #
7.1 避免过度使用Ref #
javascript
// ❌ 不应该用Ref管理状态
function Form() {
const nameRef = useRef();
const emailRef = useRef();
const handleSubmit = () => {
console.log(nameRef.current.value);
};
}
// ✅ 应该使用State
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = () => {
console.log(name);
};
}
7.2 Ref初始化时机 #
javascript
function Component() {
const ref = useRef(null);
useEffect(() => {
// ✅ 在useEffect中访问ref.current
if (ref.current) {
ref.current.focus();
}
}, []);
// ❌ 不要在渲染期间访问ref.current
if (ref.current) {
// 这会导致问题
}
return <div ref={ref}>Content</div>;
}
7.3 清理Ref #
javascript
function Component() {
const timerRef = useRef(null);
useEffect(() => {
return () => {
// 组件卸载时清理
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
return <div>...</div>;
}
八、常见问题 #
8.1 Ref为null #
javascript
function Component() {
const ref = useRef(null);
const handleClick = () => {
// ✅ 检查ref.current是否存在
if (ref.current) {
ref.current.focus();
}
};
return <div ref={ref}>...</div>;
}
8.2 函数组件没有实例 #
javascript
// ❌ 函数组件不能直接获取ref
function MyComponent() {
return <div>Content</div>;
}
function App() {
const ref = useRef();
return <MyComponent ref={ref} />; // 警告
}
// ✅ 使用forwardRef
const MyComponent = forwardRef((props, ref) => {
return <div ref={ref}>Content</div>;
});
九、总结 #
| 要点 | 说明 |
|---|---|
| useRef | 函数组件创建Ref |
| createRef | 类组件创建Ref |
| forwardRef | 转发Ref给子组件 |
| useImperativeHandle | 自定义暴露的实例值 |
核心原则:
- Ref 用于访问 DOM 和存储可变值
- 不要用 Ref 替代 State
- 访问 ref.current 前检查是否存在
- 及时清理 Ref 中的资源
最后更新:2026-03-26