说说对 React 的 refs 属性的理解?应用场景?
问题解析(面试官考察点)
面试官通过此问题主要考察:
- 对 refs 概念和作用的理解
- 对 refs 不同使用方式的掌握
- 对 refs 适用场景的了解
- 对 refs 使用注意事项的认识
核心概念(基础知识点)
什么是 Refs
Refs(references)提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。Refs 在以下场景中特别有用:
- 管理焦点、文本选择或媒体播放
- 触发强制动画
- 集成第三方 DOM 库
- 获取子组件的实例或 DOM 节点
Refs 的本质
Refs 本质上是一个对象,包含一个 current 属性,指向被引用的 DOM 节点或组件实例:
{
current: DOMElement | ComponentInstance | null
}
详细解答(代码示例)
创建 Refs 的三种方式
1. createRef(类组件推荐)
import React, { Component } from "react";
class InputFocus extends Component {
constructor(props) {
super(props);
// 创建 ref
this.inputRef = React.createRef();
}
componentDidMount() {
// 通过 current 访问 DOM 节点
this.inputRef.current.focus();
}
handleClick = () => {
// 获取 input 的值
alert(this.inputRef.current.value);
};
render() {
// 通过 ref 属性绑定
return (
<div>
<input ref={this.inputRef} type="text" placeholder="输入内容" />
<button onClick={this.handleClick}>获取值</button>
</div>
);
}
}
2. useRef(函数组件推荐)
import React, { useRef, useEffect } from "react";
function InputFocus() {
// 创建 ref
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后聚焦
inputRef.current.focus();
}, []);
const handleClick = () => {
alert(inputRef.current.value);
};
return (
<div>
<input ref={inputRef} type="text" placeholder="输入内容" />
<button onClick={handleClick}>获取值</button>
</div>
);
}
3. 回调 Refs
class CustomTextInput extends Component {
constructor(props) {
super(props);
this.inputElement = null; // 用于存储 ref
}
componentDidMount() {
// 直接访问 DOM 节点
if (this.inputElement) {
this.inputElement.focus();
}
}
render() {
return (
<div>
<input
type="text"
// 回调 ref:将 DOM 节点赋值给实例属性
ref={(element) => { this.inputElement = element; }}
/>
</div>
);
}
}
// 回调 ref 也可以传递函数
function CustomTextInputWithCallback() {
let inputElement = null;
return (
<div>
<input
type="text"
ref={(element) => { inputElement = element; }}
/>
<button onClick={() => inputElement && inputElement.focus()}>
聚焦
</button>
</div>
);
}
Refs 转发(forwardRef)
import React, { forwardRef } from "react";
// 子组件:使用 forwardRef 转发 ref
const FancyInput = forwardRef((props, ref) => {
return (
<div className="fancy-input">
<input ref={ref} type="text" {...props} />
<span className="icon">✨</span>
</div>
);
});
// 父组件
function Parent() {
const fancyInputRef = useRef(null);
const handleClick = () => {
// 直接访问子组件内部的 input
fancyInputRef.current.focus();
};
return (
<div>
<FancyInput ref={fancyInputRef} placeholder="请输入" />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}
// 在类组件中使用 forwardRef
const FancyButton = forwardRef((props, ref) => (
<button ref={ref} className="fancy-button">
{props.children}
</button>
));
class App extends React.Component {
constructor(props) {
super(props);
this.buttonRef = React.createRef();
}
render() {
return (
<FancyButton ref={this.buttonRef}>
点击我
</FancyButton>
);
}
}
useImperativeHandle(控制暴露内容)
import React, { useRef, forwardRef, useImperativeHandle } from "react";
// 使用 useImperativeHandle 控制暴露给父组件的方法
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 只暴露特定的方法给父组件
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = "";
},
getValue: () => {
return inputRef.current.value;
}
// 注意:没有暴露 inputRef.current 本身
}));
return <input ref={inputRef} type="text" {...props} />;
});
function Parent() {
const fancyInputRef = useRef(null);
const handleFocus = () => {
fancyInputRef.current.focus();
};
const handleClear = () => {
fancyInputRef.current.clear();
};
const handleGetValue = () => {
alert(fancyInputRef.current.getValue());
};
return (
<div>
<FancyInput ref={fancyInputRef} />
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空</button>
<button onClick={handleGetValue}>获取值</button>
</div>
);
}
深入理解(原理剖析)
Refs 的工作原理
┌─────────────────────────────────────────────────────────────┐
│ Refs 工作原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 创建 Ref │
│ const myRef = useRef(null); │
│ // { current: null } │
│ │
│ 2. 绑定到 JSX 元素 │
│ <input ref={myRef} /> │
│ │
│ 3. React 在渲染时处理 ref │
│ - 组件挂载后:myRef.current = DOMElement │
│ - 组件卸载后:myRef.current = null │
│ │
│ 4. 通过 current 访问 │
│ myRef.current.focus(); │
│ │
└─────────────────────────────────────────────────────────────┘
Refs 的执行时机
class RefTiming extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
console.log("constructor:", this.myRef.current); // null
}
render() {
console.log("render:", this.myRef.current); // null
return <div ref={this.myRef}>Content</div>;
}
componentDidMount() {
console.log("componentDidMount:", this.myRef.current); // DOM 节点
}
componentWillUnmount() {
console.log("componentWillUnmount:", this.myRef.current); // DOM 节点
}
}
// 函数组件中的时机
function RefTimingFunction() {
const myRef = useRef(null);
console.log("render:", myRef.current); // 首次 null,之后 DOM 节点
useEffect(() => {
console.log("useEffect:", myRef.current); // DOM 节点
});
return <div ref={myRef}>Content</div>;
}
Refs 与闭包问题
function ClosureProblem() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 错误:ref 不会自动更新
const handleClickWrong = () => {
setTimeout(() => {
alert(countRef.current); // 总是显示初始值 0
}, 3000);
};
// 正确:手动更新 ref
useEffect(() => {
countRef.current = count;
}, [count]);
const handleClickCorrect = () => {
setTimeout(() => {
alert(countRef.current); // 显示最新的 count 值
}, 3000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={handleClickWrong}>Alert (Wrong)</button>
<button onClick={handleClickCorrect}>Alert (Correct)</button>
</div>
);
}
最佳实践
1. 不要过度使用 Refs
// 不好的做法:用 ref 获取值,而不是受控组件
function BadExample() {
const inputRef = useRef(null);
const handleSubmit = () => {
// 不推荐:直接从 DOM 获取值
const value = inputRef.current.value;
console.log(value);
};
return <input ref={inputRef} />;
}
// 好的做法:使用受控组件
function GoodExample() {
const [value, setValue] = useState("");
const handleSubmit = () => {
console.log(value);
};
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
2. Refs 的适用场景
// 场景1:管理焦点
function FocusManager() {
const firstInputRef = useRef(null);
const secondInputRef = useRef(null);
const handleFirstKeyDown = (e) => {
if (e.key === "Enter") {
secondInputRef.current.focus();
}
};
return (
<div>
<input
ref={firstInputRef}
onKeyDown={handleFirstKeyDown}
placeholder="按 Enter 跳到下一个"
/>
<input ref={secondInputRef} placeholder="第二个输入框" />
</div>
);
}
// 场景2:媒体控制
function VideoPlayer() {
const videoRef = useRef(null);
const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();
return (
<div>
<video ref={videoRef} src="video.mp4" />
<button onClick={play}>播放</button>
<button onClick={pause}>暂停</button>
</div>
);
}
// 场景3:动画集成
function AnimationBox() {
const boxRef = useRef(null);
const animate = () => {
// 使用第三方动画库
gsap.to(boxRef.current, {
x: 100,
duration: 1
});
};
return (
<div>
<div ref={boxRef} className="box" />
<button onClick={animate}>动画</button>
</div>
);
}
// 场景4:测量 DOM 尺寸
function MeasureElement() {
const elementRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (elementRef.current) {
const { width, height } = elementRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={elementRef}>需要测量的内容</div>
<p>Width: {dimensions.width}, Height: {dimensions.height}</p>
</div>
);
}
3. 避免在渲染期间访问 ref
function BadExample() {
const myRef = useRef(null);
// 错误:在渲染期间访问 ref
if (myRef.current) {
myRef.current.focus();
}
return <input ref={myRef} />;
}
function GoodExample() {
const myRef = useRef(null);
// 正确:在 effect 中访问
useEffect(() => {
if (myRef.current) {
myRef.current.focus();
}
}, []);
return <input ref={myRef} />;
}
面试要点
-
Refs 是什么: 提供访问 DOM 节点或组件实例的方式
-
创建方式:
React.createRef()(类组件)useRef()(函数组件)- 回调 refs
-
转发 Refs: 使用
forwardRef将 ref 传递给子组件 -
控制暴露: 使用
useImperativeHandle控制子组件暴露的内容 -
适用场景:
- 管理焦点、文本选择、媒体播放
- 触发强制动画
- 集成第三方 DOM 库
- 测量 DOM 尺寸
-
注意事项:
- 不要过度使用 refs,优先使用受控组件
- 不要在渲染期间访问 ref.current
- 组件卸载后 ref.current 变为 null