React 版本:18.x 难度:中级
考察要点:
- useRef 的使用场景
- forwardRef 的应用
- DOM 操作的最佳实践
- 性能优化考虑
解答:
1. 概念解释
基本定义:
- Ref:访问 DOM 节点或 React 元素的引用
- useRef:创建可变引用的 Hook
- forwardRef:转发 ref 到子组件
工作原理:
- 在组件渲染周期外保持值
- 不触发组件重新渲染
- 支持命令式操作
应用场景:
- 焦点管理
- 文本选择
- 动画控制
- 媒体播放
- 第三方库集成
2. 代码示例
基础示例:
import React, { useRef, useEffect } from 'react';
interface InputProps {
autoFocus?: boolean;
placeholder?: string;
onFocus?: () => void;
}
// 👉 基础输入组件
const FocusInput: React.FC<InputProps> = ({
autoFocus,
placeholder,
onFocus
}) => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
}, [autoFocus]);
return (
<input
ref={inputRef}
type="text"
placeholder={placeholder}
onFocus={onFocus}
className="focus-input"
/>
);
};
// 👉 可转发 ref 的输入组件
const ForwardedInput = React.forwardRef<
HTMLInputElement,
InputProps
>((props, ref) => (
<input
ref={ref}
type="text"
{...props}
className="forwarded-input"
/>
));
// 👉 使用示例
export const InputExample: React.FC = () => {
const forwardedRef = useRef<HTMLInputElement>(null);
const handleFocusClick = () => {
forwardedRef.current?.focus();
};
return (
<div>
<FocusInput
autoFocus
placeholder="Auto focused input"
/>
<ForwardedInput
ref={forwardedRef}
placeholder="Forwarded ref input"
/>
<button onClick={handleFocusClick}>
Focus Forwarded Input
</button>
</div>
);
};
进阶示例:
import React, { useRef, useEffect, useState } from 'react';
interface MediaPlayerProps {
src: string;
onTimeUpdate?: (currentTime: number) => void;
onEnded?: () => void;
}
// 👉 自定义 Hook 用于媒体控制
const useMediaControls = (videoRef: React.RefObject<HTMLVideoElement>) => {
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const handleTimeUpdate = () => {
setCurrentTime(video.currentTime);
};
const handleDurationChange = () => {
setDuration(video.duration);
};
video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener('durationchange', handleDurationChange);
return () => {
video.removeEventListener('timeupdate', handleTimeUpdate);
video.removeEventListener('durationchange', handleDurationChange);
};
}, [videoRef]);
const togglePlay = () => {
const video = videoRef.current;
if (!video) return;
if (video.paused) {
video.play();
setIsPlaying(true);
} else {
video.pause();
setIsPlaying(false);
}
};
const seek = (time: number) => {
const video = videoRef.current;
if (!video) return;
video.currentTime = time;
};
return {
isPlaying,
duration,
currentTime,
togglePlay,
seek
};
};
// 👉 媒体播放器组件
export const MediaPlayer: React.FC<MediaPlayerProps> = ({
src,
onTimeUpdate,
onEnded
}) => {
const videoRef = useRef<HTMLVideoElement>(null);
const {
isPlaying,
duration,
currentTime,
togglePlay,
seek
} = useMediaControls(videoRef);
useEffect(() => {
onTimeUpdate?.(currentTime);
}, [currentTime, onTimeUpdate]);
return (
<div className="media-player">
<video
ref={videoRef}
src={src}
onEnded={onEnded}
/>
<div className="controls">
<button onClick={togglePlay}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<input
type="range"
min={0}
max={duration}
value={currentTime}
onChange={(e) => seek(Number(e.target.value))}
/>
<span>
{Math.floor(currentTime)}/{Math.floor(duration)}
</span>
</div>
</div>
);
};
3. 注意事项与最佳实践
❌ 常见错误示例:
// ❌ 错误示范:过度使用 ref 操作 DOM
const BadComponent: React.FC = () => {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// 错误:应该使用 state 控制样式
if (divRef.current) {
divRef.current.style.backgroundColor = 'red';
}
}, []);
return <div ref={divRef} />;
};
// ❌ 错误示范:在渲染期间访问 ref
const AnotherBadComponent: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
// 错误:渲染时访问 ref
const value = inputRef.current?.value || '';
return <input ref={inputRef} defaultValue={value} />;
};
✅ 正确实现方式:
// ✅ 正确示范:合理使用 ref
const GoodComponent: React.FC = () => {
const [backgroundColor, setBackgroundColor] = useState('white');
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<div style={{ backgroundColor }}>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus Input</button>
</div>
);
};
// ✅ 正确示范:结合 useImperativeHandle
interface TextInputHandle {
focus: () => void;
clear: () => void;
}
const TextInput = React.forwardRef<TextInputHandle, {}>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
return <input ref={inputRef} />;
});
4. 性能优化
import React, { useRef, useCallback, useState } from 'react';
// 👉 优化的滚动容器
const OptimizedScrollContainer: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const [isNearBottom, setIsNearBottom] = useState(false);
// 使用 useCallback 缓存滚动处理函数
const handleScroll = useCallback(() => {
const container = containerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const threshold = 100;
setIsNearBottom(
scrollHeight - scrollTop - clientHeight < threshold
);
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener('scroll', handleScroll);
return () => {
container.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
return (
<div
ref={containerRef}
style={{ height: '300px', overflow: 'auto' }}
>
{/* 内容 */}
{isNearBottom && (
<div>Near bottom!</div>
)}
</div>
);
};
5. 测试策略
import { render, screen, fireEvent } from '@testing-library/react';
describe('FocusInput', () => {
it('should focus input on mount when autoFocus is true', () => {
render(<FocusInput autoFocus />);
const input = screen.getByRole('textbox');
expect(document.activeElement).toBe(input);
});
it('should forward ref correctly', () => {
const ref = React.createRef<HTMLInputElement>();
render(<ForwardedInput ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
});
这个实现展示了 Refs 和 DOM 操作的完整使用方案,包括:
- 基础和进阶的实现方式
- 错误处理和最佳实践
- 性能优化策略
- 测试方法
关键是要理解何时使用 refs,以及如何正确处理 DOM 操作来提升组件的可维护性和性能。