Skip to content

深入理解 Ref 机制

useRef/createRef/callback ref 原理、forwardRef、ref 在 Commit 阶段的附着与清理、React 19 ref as prop

什么是 Ref?

定义:Ref 是 React 提供的逃生舱机制,用于在渲染周期之外持有可变引用。最常见的用途是获取 DOM 节点引用,但 ref 也可以存储任何不需要触发重渲染的可变值(如定时器 ID、上一次的值等)。


一、三种 Ref 用法

useRef(函数组件,推荐)

jsx
import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);
  const renderCount = useRef(0);

  // 获取 DOM
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  // 存储可变值(不触发重渲染)
  renderCount.current++;
  console.log('渲染次数:', renderCount.current);

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

createRef(类组件)

jsx
class TextInput extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef(); // 每次调用创建新对象 { current: null }
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }

  render() {
    return <input ref={this.inputRef} />;
  }
}

Callback Ref(最灵活)

jsx
function MeasuredBox() {
  const [height, setHeight] = useState(0);

  // callback ref:节点挂载时调用(参数为 DOM 节点),卸载时调用(参数为 null)
  const measuredRef = useCallback((node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div ref={measuredRef}>
      <p>这个盒子的高度是: {height}px</p>
    </div>
  );
}

// callback ref 的优势:
// 1. 可以在 ref 附着时立即执行逻辑(不需要 useEffect)
// 2. 可以处理条件渲染的节点(节点出现/消失时自动通知)
// 3. 可以同时传给多个元素

二、useRef 的内部实现

javascript
// Mount 阶段
function mountRef(initialValue) {
  const hook = mountWorkInProgressHook(); // 创建 Hook 节点
  const ref = { current: initialValue };
  hook.memoizedState = ref;
  return ref;
}

// Update 阶段
function updateRef() {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState; // 直接返回同一个对象引用
}

// 关键:
// 1. useRef 返回的对象在整个组件生命周期中保持不变(引用稳定)
// 2. 修改 .current 不会触发重渲染
// 3. .current 的变化在下次渲染时可读(但不触发渲染)

useRef vs useState

jsx
// useState:值变化 → 触发重渲染
const [count, setCount] = useState(0);
setCount(1); // 触发重渲染

// useRef:值变化 → 不触发重渲染
const countRef = useRef(0);
countRef.current = 1; // 不触发重渲染

// 使用场景:
// useState → 需要渲染到 UI 的值
// useRef → 不需要渲染的值(DOM 引用、定时器 ID、上一次的 props/state)

三、Ref 在 Commit 阶段的处理

Ref 的附着与清理时机

Render 阶段:
  beginWork 中发现 ref 属性变化
  → 标记 Fiber 的 flags |= Ref

Commit 阶段 - Mutation 子阶段(DOM 操作后):
  1. 清理旧 ref(detach)
     if (current.flags & Ref) {
       safelyDetachRef(current);
       // object ref → ref.current = null
       // callback ref → ref(null)
     }

Commit 阶段 - Layout 子阶段(DOM 已更新):
  2. 附着新 ref(attach)
     if (finishedWork.flags & Ref) {
       safelyAttachRef(finishedWork);
       // object ref → ref.current = DOM 节点
       // callback ref → ref(DOM 节点)
     }

顺序保证:
  先清理旧 ref → 操作 DOM → 附着新 ref
  → ref.current 在 useLayoutEffect 回调中已经是新的 DOM

源码简化

javascript
function commitAttachRef(finishedWork) {
  const ref = finishedWork.ref;
  const instance = finishedWork.stateNode; // DOM 节点

  if (typeof ref === 'function') {
    // Callback ref
    ref(instance);
  } else if (ref !== null) {
    // Object ref (useRef / createRef)
    ref.current = instance;
  }
}

function commitDetachRef(current) {
  const ref = current.ref;

  if (typeof ref === 'function') {
    ref(null); // 通知 callback ref 清理
  } else if (ref !== null) {
    ref.current = null;
  }
}

Ref 何时标记更新

javascript
// beginWork 中,对比新旧 ref
function markRef(current, workInProgress) {
  const ref = workInProgress.ref;

  if (
    (current === null && ref !== null) ||     // 首次挂载且有 ref
    (current !== null && current.ref !== ref)  // ref 引用变了
  ) {
    workInProgress.flags |= Ref;
  }
}

// 这就是为什么 callback ref 用 useCallback 包裹很重要:
// ❌ 每次渲染创建新函数 → ref 引用每次都变 → 每次都清理+重新附着
<div ref={(node) => { ... }} />

// ✅ useCallback 稳定引用 → ref 引用不变 → 不重复清理附着
const callbackRef = useCallback((node) => { ... }, []);
<div ref={callbackRef} />

四、forwardRef

为什么需要 forwardRef?

jsx
// ❌ ref 不会作为 props 传递(React 16-18)
function MyInput(props) {
  // props.ref 是 undefined!
  return <input ref={props.ref} />;
}

// ref 是 React 的特殊属性,和 key 一样会被 React 内部消费,不传给组件

// ✅ forwardRef 将 ref 转发给子组件
const MyInput = forwardRef(function MyInput(props, ref) {
  return <input ref={ref} className="custom-input" {...props} />;
});

// 使用
function Form() {
  const inputRef = useRef(null);
  return <MyInput ref={inputRef} placeholder="请输入" />;
  // inputRef.current → <input> DOM 节点
}

forwardRef 内部原理

javascript
function forwardRef(render) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render, // 原始的渲染函数 (props, ref) => JSX
  };
}

// beginWork 中处理 ForwardRef
function updateForwardRef(current, workInProgress, renderLanes) {
  const render = workInProgress.type.render;
  const props = workInProgress.pendingProps;
  const ref = workInProgress.ref; // React 从 JSX 中提取的 ref

  // 将 ref 作为第二个参数传给 render 函数
  const children = render(props, ref);

  reconcileChildren(current, workInProgress, children, renderLanes);
  return workInProgress.child;
}

五、React 19:ref as prop

不再需要 forwardRef

jsx
// React 19+:ref 直接作为 props 传递
function MyInput({ ref, ...props }) {
  return <input ref={ref} className="custom-input" {...props} />;
}

// 直接使用,无需 forwardRef 包裹
function Form() {
  const inputRef = useRef(null);
  return <MyInput ref={inputRef} placeholder="请输入" />;
}

内部变化

javascript
// React 19 之前:
// ref 从 JSX 中提取,存在 fiber.ref 上,不传入 props

// React 19:
// ref 仍然存在 fiber.ref 上(用于 Commit 阶段的附着/清理)
// 但同时也作为 props.ref 传给函数组件
// → 函数组件可以直接从 props 中解构 ref
// → forwardRef 变成可选的(保持向后兼容)

六、useImperativeHandle

jsx
// 自定义暴露给父组件的 ref 接口
const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);

  // 不暴露完整的 DOM 节点,只暴露需要的方法
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }), []);

  return <input ref={inputRef} {...props} />;
});

// 父组件
function Form() {
  const inputRef = useRef(null);

  const handleSubmit = () => {
    console.log(inputRef.current.getValue()); // 自定义方法
    inputRef.current.clear();
    inputRef.current.focus();
    // inputRef.current.style → undefined(未暴露)
  };

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

useImperativeHandle 原理

javascript
function useImperativeHandle(ref, create, deps) {
  // 本质是一个 useLayoutEffect
  useLayoutEffect(() => {
    const instance = create(); // 执行工厂函数获取暴露的接口

    if (typeof ref === 'function') {
      ref(instance);
      return () => ref(null);
    } else if (ref !== null) {
      ref.current = instance;
      return () => { ref.current = null; };
    }
  }, deps);
}

// 在 Layout 阶段执行 → DOM 已更新 → 内部 ref 已附着
// 所以 create 函数中可以安全地访问内部的 DOM ref

七、常见模式

获取上一次的值

jsx
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value; // 渲染后更新
  });
  return ref.current; // 返回渲染前的值(上一次的)
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <p>当前: {count},上次: {prevCount}</p>;
}

Ref 回调实现列表 DOM 收集

jsx
function ItemList({ items }) {
  const itemRefs = useRef(new Map());

  const scrollToItem = (id) => {
    const node = itemRefs.current.get(id);
    node?.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    <ul>
      {items.map(item => (
        <li
          key={item.id}
          ref={(node) => {
            if (node) {
              itemRefs.current.set(item.id, node);
            } else {
              itemRefs.current.delete(item.id);
            }
          }}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

面试高频题

Q: useRef 和 useState 的区别?

  • useState:值变化触发重渲染,值在渲染闭包中是不可变的
  • useRef:值变化不触发重渲染.current 是可变引用,随时可读写
  • useRef 返回的对象引用在整个组件生命周期中保持不变

Q: Ref 是在什么时机被赋值的?

在 Commit 阶段的 Layout 子阶段,DOM 已经更新到屏幕后。先在 Mutation 阶段清理旧 ref(设为 null),再在 Layout 阶段附着新 ref(设为 DOM 节点)。因此在 useLayoutEffect 中可以安全地通过 ref 访问 DOM。

Q: React 19 的 ref as prop 和 forwardRef 有什么区别?

React 19 允许函数组件直接从 props 中接收 ref,无需 forwardRef 包裹。forwardRef 保持向后兼容但不再必需。内部实现上,React 仍然将 ref 存在 fiber.ref 上用于 Commit 阶段处理,但同时也将其作为 props 传给组件函数。

Q: 为什么 callback ref 要用 useCallback 包裹?

不用 useCallback 的话,每次渲染都创建新函数,ref 引用变化会导致 React 在 Commit 阶段先调用旧的 callback ref(传 null 清理),再调用新的(传 DOM 附着)。用 useCallback 保持引用稳定,避免不必要的清理-附着循环。