在React 中的 refs 何时应该使用回调形式(callback ref)?它与 useRef 有什么区别?

大白话在React 中的 refs 何时应该使用回调形式(callback ref)?它与 useRef 有什么区别?

前端小伙伴们,是不是在React开发中被refs搞晕过?尤其遇到callback refuseRef时,总纠结“这俩到底有啥区别?什么时候该用谁?”今天咱们用大白话唠明白,包你看完就能上手!


一、这些坑你踩过吗?

先说说我刚学React时踩的坑:
有次做一个动态列表,想在列表项渲染完成后自动聚焦新添加的输入框。用useRef绑定输入框,结果发现ref.current总是拿不到最新的DOM——因为组件重新渲染时,useRefcurrent属性虽然不会变,但新的DOM节点还没挂载!
还有一次给子组件传ref,子组件是动态变化的(比如条件渲染不同组件),用useRef拿不到最新的子组件实例,急得直挠头……

相信不少同学遇到过类似情况:需要在组件挂载、更新、卸载的“关键点”操作DOM或组件实例,这时候callback ref就派上用场了!


二、先搞懂React refs的“底层逻辑”

React的refs是用来访问DOM节点或组件实例的“通道”,官方推荐过三种形式:

  • 字符串ref(已弃用,不推荐)
  • 回调ref(callback ref):通过函数传递ref
  • 对象ref:通过useRef(函数组件)或createRef(类组件)创建

今天重点聊后两种:callback refuseRef

1. callback ref:“生命周期感知”的ref

核心逻辑callback ref是一个函数,React会在组件挂载时调用该函数(参数是DOM节点或组件实例),卸载时再次调用(参数是null)。如果组件更新导致ref属性变化(比如父组件传递的ref函数变了),React会先调用旧的ref(传null),再调用新的ref(传新节点)。

简单说,它能“感知”组件的挂载、更新、卸载全流程,适合需要在这些关键时间点执行操作的场景。

2. useRef:“静态存储”的ref

useRef是React的一个钩子,返回一个可变对象,其current属性可以存储任意值(比如DOM节点、组件实例、甚至普通变量)。这个对象在组件的整个生命周期中保持不变(每次渲染都是同一个对象)。

它的核心作用是跨渲染周期存储数据,但不会触发组件重新渲染。常见用法是存储DOM节点、定时器ID、或需要在多次渲染间保留的状态。


三、代码示例:用例子说话

示例1:需要“感知”DOM挂载/卸载时——用callback ref

比如:页面加载时自动聚焦输入框,页面卸载时打印“输入框已销毁”。
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} />;
  }
}

示例2:需要“跨渲染周期存储”时——用useRef

比如:记录按钮被点击的次数(不触发重新渲染),或存储定时器ID。
useRefcurrent属性像一个“盒子”,可以随时修改,且不会影响组件渲染:

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>
  );
}

示例3:动态子组件需要“实时获取实例”时——用callback ref

比如:父组件需要根据条件渲染不同的子组件(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会有问题:因为useRefcurrent属性只会在挂载时赋值一次,当子组件切换时,current不会自动更新为新的实例;而callback ref会在旧组件卸载(传null)和新组件挂载(传新实例)时分别调用,确保拿到最新的实例。


四、一张表总结区别

对比项 callback ref useRef
本质 函数(React生命周期钩子) 对象({ current: … })
触发时机 挂载、更新(ref变化时)、卸载时调用 组件渲染时返回同一个对象,current手动修改
适用场景 需要感知DOM/组件的挂载、卸载、更新过程 存储DOM节点、跨渲染周期的变量、不触发渲染的状态
值的变化监听 自动触发回调(可直接在回调中操作) 需手动监听current变化(比如用effect)
组件类型 函数组件、类组件均可 仅函数组件(钩子限制)
性能注意 频繁更新ref可能导致回调频繁执行(需优化) 无性能问题(对象引用不变)

五、面试回答方法

面试被问“什么时候用callback ref?和useRef有啥区别?”,可以这样答:

“简单来说,callback ref是‘活的’,useRef是‘静的’

  • 当需要感知DOM或组件的挂载、卸载、更新过程时(比如自动聚焦、卸载时清理操作、动态子组件获取实例),用callback ref。它会在这些关键时间点触发回调,能实时拿到最新的节点或实例。
  • 当需要跨渲染周期存储数据(比如DOM节点、定时器ID、不触发渲染的状态)时,用useRef。它返回一个不变的对象,current属性可以随时修改,适合存静态数据。

举个例子:如果我要在输入框挂载时自动聚焦,用callback ref,因为它挂载时会触发回调执行focus();如果我要记录按钮点击次数(不触发重新渲染),用useRef,因为current改了不影响渲染。”


六、总结:一句话记住使用场景

用callback ref:需要“参与”组件生命周期(挂载/更新/卸载)时;
用useRef:需要“存储”跨渲染周期的数据时。


七、扩展思考(4个高频问题)

问题1:callback ref在函数组件和类组件中表现一致吗?

一致。无论是函数组件还是类组件,React处理callback ref的逻辑都是一样的:挂载时传节点,卸载时传null。区别在于类组件中callback ref通常是实例方法(如this.inputRef),而函数组件中是内联函数或通过useCallback优化的函数。

问题2:useRef能监听current的变化吗?如何实现?

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>;
}

问题3:什么时候必须用callback ref而不是useRef?

当需要在ref更新时执行副作用(比如根据新的DOM节点调整布局、同步子组件状态)。例如:
父组件动态调整子组件的宽度,需要在子组件DOM更新后立即获取新宽度并计算布局。这时候用callback ref可以在子组件挂载/更新时触发回调,直接拿到最新的DOM节点;而用useRef需要手动在useEffect中监听current变化,代码更复杂。

问题4:callback ref会导致性能问题吗?如何优化?

如果在函数组件中内联定义callback ref(如ref={(node) => {...}}),每次渲染都会创建新的函数,可能导致子组件不必要的重新渲染(因为ref属性变化了)。
优化方法:用useCallback包裹callback ref,确保函数引用不变:

function InputWithOptimizedCallback() {
  // 用useCallback缓存回调函数(依赖项为空,函数引用不变)
  const inputRef = useCallback((node) => {
    if (node) node.focus();
  }, []); // 空依赖数组,只创建一次函数

  return <input ref={inputRef} />;
}

轻松搞定React refs

现在是不是觉得callback refuseRef没那么难了?记住核心:需要“感知生命周期”用callback ref,需要“存储数据”用useRef。下次遇到refs问题,先想场景再选工具,保证少踩坑!

如果觉得有用,点个赞收藏吧~有其他React问题,评论区见!

你可能感兴趣的:(大白话前端八股,react.js,前端,javascript)