Ref与DOM操作 #
一、Ref 概述 #
1.1 什么是 Ref #
Ref 提供了一种访问 DOM 元素或组件实例的方式,绕过典型的 Props 数据流。
1.2 使用场景 #
| 场景 | 说明 |
|---|---|
| 焦点管理 | 自动聚焦输入框 |
| 文本选择 | 选择输入文本 |
| 媒体控制 | 播放/暂停视频 |
| 动画 | 触发动画 |
| 第三方库 | 集成非 Preact 库 |
1.3 Ref vs State #
| 方面 | Ref | State |
|---|---|---|
| 更新触发 | 不触发渲染 | 触发渲染 |
| 用途 | DOM/实例引用 | 组件数据 |
| 可变性 | 可直接修改 | 通过 setter |
二、useRef #
2.1 基本用法 #
jsx
import { useRef } from 'preact/hooks';
function TextInput() {
const inputRef = useRef(null);
const focus = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focus}>Focus Input</button>
</div>
);
}
2.2 访问 DOM 元素 #
jsx
function MeasureElement() {
const divRef = useRef(null);
const measure = () => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
console.log(`Width: ${width}, Height: ${height}`);
}
};
return (
<div>
<div
ref={divRef}
style={{ width: '200px', height: '100px', background: '#f0f0f0' }}
>
Content
</div>
<button onClick={measure}>Measure</button>
</div>
);
}
2.3 存储可变值 #
jsx
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const reset = () => {
stop();
setTime(0);
};
return (
<div>
<p>{time} seconds</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
2.4 获取前一次的值 #
jsx
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>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
三、createRef(类组件) #
3.1 基本用法 #
jsx
import { Component, createRef } from 'preact';
class TextInput extends Component {
inputRef = createRef();
focus = () => {
this.inputRef.current.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} type="text" />
<button onClick={this.focus}>Focus</button>
</div>
);
}
}
3.2 回调 Ref #
jsx
class TextInput extends Component {
inputElement = null;
setInputRef = (element) => {
this.inputElement = element;
};
focus = () => {
if (this.inputElement) {
this.inputElement.focus();
}
};
render() {
return (
<div>
<input ref={this.setInputRef} type="text" />
<button onClick={this.focus}>Focus</button>
</div>
);
}
}
四、常见使用场景 #
4.1 自动聚焦 #
jsx
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Auto focused" />;
}
4.2 文本选择 #
jsx
function SelectableInput({ value }) {
const inputRef = useRef(null);
const selectAll = () => {
inputRef.current?.select();
};
return (
<div>
<input ref={inputRef} value={value} readOnly />
<button onClick={selectAll}>Select All</button>
</div>
);
}
4.3 视频控制 #
jsx
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const play = () => {
videoRef.current?.play();
};
const pause = () => {
videoRef.current?.pause();
};
const restart = () => {
if (videoRef.current) {
videoRef.current.currentTime = 0;
videoRef.current.play();
}
};
return (
<div>
<video ref={videoRef} src={src} />
<div>
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
<button onClick={restart}>Restart</button>
</div>
</div>
);
}
4.4 滚动控制 #
jsx
function ScrollContainer() {
const containerRef = useRef(null);
const scrollToTop = () => {
containerRef.current?.scrollTo({
top: 0,
behavior: 'smooth'
});
};
const scrollToBottom = () => {
if (containerRef.current) {
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight,
behavior: 'smooth'
});
}
};
return (
<div>
<div
ref={containerRef}
style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}
>
{/* Long content */}
{Array.from({ length: 20 }, (_, i) => (
<p key={i}>Item {i + 1}</p>
))}
</div>
<button onClick={scrollToTop}>Scroll to Top</button>
<button onClick={scrollToBottom}>Scroll to Bottom</button>
</div>
);
}
4.5 Canvas 操作 #
jsx
function CanvasDrawing() {
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
}, []);
const startDrawing = (e) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
setIsDrawing(true);
};
const draw = (e) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
ctx.stroke();
};
const stopDrawing = () => {
setIsDrawing(false);
};
const clear = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
return (
<div>
<canvas
ref={canvasRef}
width={400}
height={300}
style={{ border: '1px solid #ccc' }}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
/>
<button onClick={clear}>Clear</button>
</div>
);
}
五、转发 Ref #
5.1 forwardRef #
jsx
import { forwardRef } from 'preact';
const FancyInput = forwardRef((props, ref) => {
return (
<div class="fancy-input">
<input ref={ref} {...props} class="input" />
<span class="icon">🔍</span>
</div>
);
});
// 使用
function Form() {
const inputRef = useRef(null);
const focus = () => {
inputRef.current?.focus();
};
return (
<div>
<FancyInput ref={inputRef} placeholder="Search..." />
<button onClick={focus}>Focus</button>
</div>
);
}
5.2 useImperativeHandle #
jsx
import { forwardRef, useImperativeHandle, useRef } from 'preact';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
},
getValue: () => {
return inputRef.current?.value;
}
}));
return <input ref={inputRef} {...props} />;
});
// 使用
function Form() {
const inputRef = useRef(null);
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => inputRef.current?.clear()}>Clear</button>
<button onClick={() => console.log(inputRef.current?.getValue())}>
Log Value
</button>
</div>
);
}
六、集成第三方库 #
6.1 集成 jQuery 插件 #
jsx
function jQueryDatePicker({ onSelect }) {
const inputRef = useRef(null);
useEffect(() => {
const $input = $(inputRef.current);
$input.datepicker({
onSelect: (date) => {
onSelect?.(date);
}
});
return () => {
$input.datepicker('destroy');
};
}, [onSelect]);
return <input ref={inputRef} type="text" />;
}
6.2 集成 D3.js #
jsx
function D3Chart({ data }) {
const svgRef = useRef(null);
useEffect(() => {
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove();
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.r)
.attr('fill', 'steelblue');
}, [data]);
return <svg ref={svgRef} width={400} height={300} />;
}
6.3 集成 Chart.js #
jsx
function Chart({ type, data, options }) {
const canvasRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
if (chartRef.current) {
chartRef.current.destroy();
}
const ctx = canvasRef.current.getContext('2d');
chartRef.current = new Chart(ctx, {
type,
data,
options
});
return () => {
chartRef.current?.destroy();
};
}, [type, data, options]);
return <canvas ref={canvasRef} />;
}
七、最佳实践 #
7.1 避免过度使用 #
jsx
// ❌ 不应该用 ref 来做这个
function BadExample() {
const inputRef = useRef(null);
const getValue = () => {
return inputRef.current?.value;
};
return <input ref={inputRef} />;
}
// ✅ 应该使用 state
function GoodExample() {
const [value, setValue] = useState('');
return (
<input
value={value}
onInput={(e) => setValue(e.target.value)}
/>
);
}
7.2 Ref 回调时机 #
jsx
function RefCallbackExample() {
const [show, setShow] = useState(false);
const setRef = (element) => {
console.log('Ref callback:', element);
// element 可能为 null(卸载时)
};
return (
<div>
<button onClick={() => setShow(!show)}>Toggle</button>
{show && <div ref={setRef}>Content</div>}
</div>
);
}
7.3 检查 Ref 是否存在 #
jsx
function SafeRefUsage() {
const ref = useRef(null);
const doSomething = () => {
// ✅ 安全检查
if (ref.current) {
ref.current.focus();
}
// ✅ 可选链
ref.current?.focus();
};
return <input ref={ref} />;
}
八、总结 #
| 要点 | 说明 |
|---|---|
| useRef | 函数组件中使用 |
| createRef | 类组件中使用 |
| forwardRef | 转发 ref 给子组件 |
| useImperativeHandle | 自定义暴露的方法 |
核心原则:
- 优先使用 state,必要时使用 ref
- 记得检查 ref.current 是否存在
- 清理第三方库实例
- 使用 forwardRef 暴露组件方法
最后更新:2026-03-28