说说 React 中 ref 的用法?应用场景?
问题解析
面试官考察点:
- 是否理解 ref 的概念和用途
- 是否掌握 ref 的创建和使用方式
- 是否了解 ref 转发
- 对 ref 使用场景的理解
核心概念
什么是 Ref
Ref(reference)提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在 React 中,数据通常通过 props 自上而下传递,但某些情况下需要在组件间直接交互(如获取焦点、触发动画等),这时可以使用 ref。
Ref 的本质
- 对于 HTML 元素:ref 指向底层的 DOM 节点
- 对于类组件:ref 指向组件的实例
- 对于函数组件:默认没有实例,需要使用 forwardRef 或 useImperativeHandle
详细解答
1. 创建 Ref 的三种方式
方式一:createRef(类组件推荐)
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
// 创建 ref
this.myRef = React.createRef();
}
componentDidMount() {
// 访问 ref
console.log(this.myRef.current);
this.myRef.current.focus();
}
render() {
// 绑定 ref
return <input ref={this.myRef} />;
}
}
方式二:useRef(函数组件推荐)
import React, { useRef, useEffect } from 'react';
function MyComponent() {
const myRef = useRef(null);
useEffect(() => {
// 访问 DOM 节点
myRef.current.focus();
}, []);
return <input ref={myRef} />;
}
方式三:回调 Ref(灵活但已较少使用)
class MyComponent extends Component {
componentDidMount() {
// 直接访问
this.inputElement.focus();
}
render() {
return (
<input
ref={(element) => { this.inputElement = element; }}
/>
);
}
}
2. Ref 的常见使用场景
管理焦点、文本选择或媒体播放
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 让 input 获得焦点
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
获取表单值(非受控组件)
function UncontrolledForm() {
const inputRef = useRef(null);
const fileInputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// 直接获取 DOM 值
console.log('Input value:', inputRef.current.value);
console.log('File:', fileInputRef.current.files[0]);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} type="text" defaultValue="Initial" />
<input ref={fileInputRef} type="file" />
<button type="submit">Submit</button>
</form>
);
}
触发强制动画
function AnimatedBox() {
const boxRef = useRef(null);
const animate = () => {
const box = boxRef.current;
box.style.transition = 'transform 0.5s';
box.style.transform = 'translateX(100px)';
setTimeout(() => {
box.style.transform = 'translateX(0)';
}, 500);
};
return (
<>
<div ref={boxRef} style={{ width: 50, height: 50, background: 'red' }} />
<button onClick={animate}>Animate</button>
</>
);
}
集成第三方 DOM 库
function ChartComponent({ data }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
// 初始化第三方图表库
chartInstance.current = new Chart(chartRef.current, {
type: 'line',
data: data
});
return () => {
// 清理
chartInstance.current.destroy();
};
}, []);
useEffect(() => {
// 数据更新时更新图表
if (chartInstance.current) {
chartInstance.current.data = data;
chartInstance.current.update();
}
}, [data]);
return <canvas ref={chartRef} />;
}
3. Ref 转发(forwardRef)
import React, { forwardRef } from 'react';
// 函数组件默认不能接受 ref,需要使用 forwardRef
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 (
<>
<FancyInput ref={inputRef} placeholder="Enter text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
4. useImperativeHandle 自定义暴露内容
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 自定义暴露给父组件的方法和属性
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
},
getValue: () => {
return inputRef.current.value;
}
}));
return <input ref={inputRef} {...props} />;
});
// 使用
function Parent() {
const fancyInputRef = useRef();
const handleClick = () => {
fancyInputRef.current.focus();
console.log(fancyInputRef.current.getValue());
// fancyInputRef.current.clear();
};
return (
<>
<FancyInput ref={fancyInputRef} />
<button onClick={handleClick}>Interact</button>
</>
);
}
5. 在类组件中使用 Ref
// 父组件获取子组件实例
class Parent extends React.Component {
childRef = React.createRef();
handleClick = () => {
// 调用子组件方法
this.childRef.current.customMethod();
};
render() {
return (
<>
<ChildComponent ref={this.childRef} />
<button onClick={this.handleClick}>Call Child Method</button>
</>
);
}
}
class ChildComponent extends React.Component {
customMethod = () => {
console.log('Method called from parent');
};
render() {
return <div>Child Component</div>;
}
}
深入理解
Ref 的更新时机
function RefTiming() {
const ref = useRef(null);
console.log('During render:', ref.current); // null
useEffect(() => {
console.log('In useEffect:', ref.current); // DOM node
});
useLayoutEffect(() => {
console.log('In useLayoutEffect:', ref.current); // DOM node
});
return <div ref={ref}>Content</div>;
}
Ref 作为可变容器
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
const countRef = useRef(0);
// 保存最新的 count 值,不触发重渲染
countRef.current = count;
const startTimer = () => {
timerRef.current = setInterval(() => {
// 可以访问到最新的 count
console.log(countRef.current);
}, 1000);
};
const stopTimer = () => {
clearInterval(timerRef.current);
};
useEffect(() => {
return () => stopTimer();
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
最佳实践
1. 不要过度使用 Ref
// 不推荐:用 ref 控制组件状态
function BadExample() {
const inputRef = useRef();
const getValue = () => {
return inputRef.current.value; // 非受控模式
};
}
// 推荐:使用受控组件
function GoodExample() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
2. 清理工作
function ResizableComponent() {
const elementRef = useRef(null);
const observerRef = useRef(null);
useEffect(() => {
observerRef.current = new ResizeObserver((entries) => {
// 处理尺寸变化
});
observerRef.current.observe(elementRef.current);
return () => {
// 清理
observerRef.current.disconnect();
};
}, []);
return <div ref={elementRef}>Resizable content</div>;
}
3. 避免在渲染期间访问 ref
function BadExample() {
const myRef = useRef(null);
// 错误:在渲染期间读取 ref
const value = myRef.current?.value;
return <input ref={myRef} />;
}
function GoodExample() {
const myRef = useRef(null);
const [value, setValue] = useState('');
useEffect(() => {
// 正确:在 effect 中读取 ref
setValue(myRef.current?.value);
}, []);
return <input ref={myRef} />;
}
应用场景总结
| 场景 | 示例 |
|---|---|
| 管理焦点 | 自动聚焦输入框 |
| 文本选择 | 高亮选中文本 |
| 媒体控制 | 播放/暂停视频 |
| 动画触发 | 强制触发动画 |
| 第三方集成 | 集成图表、地图库 |
| 测量 DOM | 获取元素尺寸位置 |
| 保存可变值 | 保存定时器 ID、上一次的值 |
面试要点
- ref 的用途:访问 DOM、获取组件实例、保存可变值
- 创建方式:createRef、useRef、回调 ref
- ref 转发:forwardRef 让函数组件接受 ref
- useImperativeHandle:自定义暴露给父组件的内容
- 注意事项:不要滥用 ref,优先使用 props 和 state
常见追问:
- ref 和 state 有什么区别?
- 为什么函数组件需要使用 forwardRef?
- useImperativeHandle 的作用是什么?
- ref 在渲染期间可以访问吗?