前端小伙伴们,是不是在React开发中被refs
搞晕过?尤其遇到callback ref
和useRef
时,总纠结“这俩到底有啥区别?什么时候该用谁?”今天咱们用大白话唠明白,包你看完就能上手!
先说说我刚学React时踩的坑:
有次做一个动态列表,想在列表项渲染完成后自动聚焦新添加的输入框。用useRef
绑定输入框,结果发现ref.current
总是拿不到最新的DOM——因为组件重新渲染时,useRef
的current
属性虽然不会变,但新的DOM节点还没挂载!
还有一次给子组件传ref
,子组件是动态变化的(比如条件渲染不同组件),用useRef
拿不到最新的子组件实例,急得直挠头……
相信不少同学遇到过类似情况:需要在组件挂载、更新、卸载的“关键点”操作DOM或组件实例,这时候callback ref
就派上用场了!
React的refs
是用来访问DOM节点或组件实例的“通道”,官方推荐过三种形式:
useRef
(函数组件)或createRef
(类组件)创建今天重点聊后两种:callback ref
和useRef
。
核心逻辑:callback ref
是一个函数,React会在组件挂载时调用该函数(参数是DOM节点或组件实例),卸载时再次调用(参数是null
)。如果组件更新导致ref
属性变化(比如父组件传递的ref
函数变了),React会先调用旧的ref
(传null
),再调用新的ref
(传新节点)。
简单说,它能“感知”组件的挂载、更新、卸载全流程,适合需要在这些关键时间点执行操作的场景。
useRef
是React的一个钩子,返回一个可变对象,其current
属性可以存储任意值(比如DOM节点、组件实例、甚至普通变量)。这个对象在组件的整个生命周期中保持不变(每次渲染都是同一个对象)。
它的核心作用是跨渲染周期存储数据,但不会触发组件重新渲染。常见用法是存储DOM节点、定时器ID、或需要在多次渲染间保留的状态。
比如:页面加载时自动聚焦输入框,页面卸载时打印“输入框已销毁”。
用callback ref
能轻松实现,因为它能在挂载和卸载时触发回调:
// 函数组件
function InputWithCallbackRef() {
// 定义callback ref函数
const inputRef = (node) => {
if (node) {
// 挂载时:自动聚焦
node.focus();
console.log('输入框已挂载');
} else {
// 卸载时:打印日志
console.log('输入框已卸载');
}
};
return <input ref={inputRef} />;
}
// 类组件同理
class InputWithCallbackRefClass extends React.Component {
// 类组件中callback ref可以是实例方法
inputRef = (node) => {
if (node) node.focus();
};
render() {
return <input ref={this.inputRef} />;
}
}
比如:记录按钮被点击的次数(不触发重新渲染),或存储定时器ID。
useRef
的current
属性像一个“盒子”,可以随时修改,且不会影响组件渲染:
function CounterWithUseRef() {
// 用useRef存储点击次数(初始为0)
const countRef = useRef(0);
// 用useRef存储DOM节点
const buttonRef = useRef(null);
const handleClick = () => {
countRef.current++; // 修改current不会触发重新渲染
console.log(`点击次数:${countRef.current}`);
// 访问DOM节点
buttonRef.current.style.backgroundColor = 'skyblue';
};
return (
<button ref={buttonRef} onClick={handleClick}>
点击我
</button>
);
}
比如:父组件需要根据条件渲染不同的子组件(A或B),并实时获取子组件的实例:
function DynamicChild() {
const [showA, setShowA] = useState(true);
// callback ref函数:获取子组件实例
const childRef = (instance) => {
if (instance) {
console.log('获取到子组件实例:', instance);
// 可以调用子组件的方法(比如子组件有一个printInfo方法)
instance.printInfo();
}
};
return (
<div>
<button onClick={() => setShowA(!showA)}>切换子组件</button>
{showA ? (
<ChildA ref={childRef} /> // ChildA是类组件或forwardRef的函数组件
) : (
<ChildB ref={childRef} /> // ChildB同理
)}
</div>
);
}
这里用useRef
会有问题:因为useRef
的current
属性只会在挂载时赋值一次,当子组件切换时,current
不会自动更新为新的实例;而callback ref
会在旧组件卸载(传null
)和新组件挂载(传新实例)时分别调用,确保拿到最新的实例。
对比项 | callback ref | useRef |
---|---|---|
本质 | 函数(React生命周期钩子) | 对象({ current: … }) |
触发时机 | 挂载、更新(ref变化时)、卸载时调用 | 组件渲染时返回同一个对象,current 手动修改 |
适用场景 | 需要感知DOM/组件的挂载、卸载、更新过程 | 存储DOM节点、跨渲染周期的变量、不触发渲染的状态 |
值的变化监听 | 自动触发回调(可直接在回调中操作) | 需手动监听current 变化(比如用effect) |
组件类型 | 函数组件、类组件均可 | 仅函数组件(钩子限制) |
性能注意 | 频繁更新ref可能导致回调频繁执行(需优化) | 无性能问题(对象引用不变) |
面试被问“什么时候用callback ref?和useRef有啥区别?”,可以这样答:
“简单来说,callback ref是‘活的’,useRef是‘静的’。
current
属性可以随时修改,适合存静态数据。举个例子:如果我要在输入框挂载时自动聚焦,用callback ref,因为它挂载时会触发回调执行focus()
;如果我要记录按钮点击次数(不触发重新渲染),用useRef,因为current
改了不影响渲染。”
用callback ref:需要“参与”组件生命周期(挂载/更新/卸载)时;
用useRef:需要“存储”跨渲染周期的数据时。
一致。无论是函数组件还是类组件,React处理callback ref的逻辑都是一样的:挂载时传节点,卸载时传null
。区别在于类组件中callback ref通常是实例方法(如this.inputRef
),而函数组件中是内联函数或通过useCallback
优化的函数。
useRef
本身不监听current
的变化(改了current
不会触发组件重新渲染)。如果需要监听,需要结合useEffect
:
function Example() {
const countRef = useRef(0);
// 监听countRef.current的变化
useEffect(() => {
console.log(`countRef变化为:${countRef.current}`);
}, [countRef.current]); // 依赖项是current的值
const handleClick = () => {
countRef.current++;
};
return <button onClick={handleClick}>点击</button>;
}
当需要在ref更新时执行副作用(比如根据新的DOM节点调整布局、同步子组件状态)。例如:
父组件动态调整子组件的宽度,需要在子组件DOM更新后立即获取新宽度并计算布局。这时候用callback ref可以在子组件挂载/更新时触发回调,直接拿到最新的DOM节点;而用useRef需要手动在useEffect
中监听current
变化,代码更复杂。
如果在函数组件中内联定义callback ref(如ref={(node) => {...}}
),每次渲染都会创建新的函数,可能导致子组件不必要的重新渲染(因为ref
属性变化了)。
优化方法:用useCallback
包裹callback ref,确保函数引用不变:
function InputWithOptimizedCallback() {
// 用useCallback缓存回调函数(依赖项为空,函数引用不变)
const inputRef = useCallback((node) => {
if (node) node.focus();
}, []); // 空依赖数组,只创建一次函数
return <input ref={inputRef} />;
}
现在是不是觉得callback ref
和useRef
没那么难了?记住核心:需要“感知生命周期”用callback ref,需要“存储数据”用useRef。下次遇到refs问题,先想场景再选工具,保证少踩坑!
如果觉得有用,点个赞收藏吧~有其他React问题,评论区见!