useRef的实现原理

286 阅读3分钟

在React中,useRef的实现依赖于React的Hooks架构和Fiber节点的内部机制。以下是useRef的实现逻辑分步说明:


1. Hooks的底层数据结构

React通过单向链表管理组件的所有Hooks。每个函数组件首次渲染时,会创建一个对应的Fiber节点,其中包含一个memoizedState属性,指向该组件的Hooks链表。

type Hook = {
  memoizedState: any,     // 当前Hook保存的状态(如useState的值、useRef的ref对象)
  baseState: any,         // 基础状态(用于更新计算)
  queue: UpdateQueue<any> | null, // 更新队列(如useState的setState触发)
  next: Hook | null,      // 指向下一个Hook的指针
};

2. useRef的初始化(Mount阶段)

当组件首次渲染时,useRef会执行以下步骤:

步骤1:创建Hook节点

  • React检查当前是否是组件的首次渲染(即mount阶段)。
  • 在Hooks链表中新建一个节点,类型标记为Ref

步骤2:生成Ref对象

  • 调用mountRef函数,创建一个普通JavaScript对象,其current属性初始化为传入的初始值。
    function mountRef<T>(initialValue: T): { current: T } {
      const hook = createHookNode(); // 创建新的Hook节点
      const ref = { current: initialValue };
      hook.memoizedState = ref;      // 将ref对象存入Hook的memoizedState
      return ref;
    }
    

步骤3:绑定到Fiber节点

  • 将该Hook节点链接到当前组件的Hooks链表中。

3. useRef的更新(Update阶段)

当组件重新渲染时,useRef的行为:

步骤1:定位Hook节点

  • React根据Hooks的调用顺序,通过链表指针找到对应的useRef Hook节点。

步骤2:直接返回已存在的Ref

  • 无需重新创建对象,直接返回之前保存的memoizedState(即同一个ref对象)。
    function updateRef<T>(): { current: T } {
      const hook = getCurrentHook(); // 从链表中获取当前Hook节点
      return hook.memoizedState;     // 返回之前存储的ref对象
    }
    

4. 关键设计点

a. 跨渲染周期持久化

  • Ref对象的引用地址在组件生命周期内不变,因此修改ref.current不会触发重新渲染。
  • 普通变量因函数组件重新执行会被重置,而useRef通过Fiber节点的持久化存储解决了这一问题。

b. 与DOM的关联

  • ref属性传递给DOM元素时,React在commit阶段处理:
    // ReactDOM处理ref的更新
    function commitAttachRef(fiber: Fiber) {
      const ref = fiber.ref;
      if (ref !== null) {
        const instance = fiber.stateNode; // 获取DOM节点
        ref.current = instance;           // 赋值给ref.current
      }
    }
    

c. 与useState的区别

特性useRefuseState
触发渲染修改.current不触发重新渲染修改状态触发重新渲染
存储位置数据保存在Hook的memoizedState状态保存在Hook的memoizedStatequeue
用途保存与渲染无关的引用(如DOM、定时器)保存影响渲染的状态数据

5. 源码简化实现

以下是useRef的极简核心逻辑(非React官方代码,仅展示原理):

let currentFiber = null; // 当前正在渲染的Fiber节点

function useRef(initialValue) {
  // 1. 获取当前Hook节点
  const hook = getNextHook(); // 根据调用顺序获取或创建Hook

  // 2. Mount阶段:初始化ref对象
  if (hook.memoizedState === undefined) {
    hook.memoizedState = { current: initialValue };
  }

  // 3. 返回持久化的ref对象
  return hook.memoizedState;
}

// 示例:组件使用useRef
function Component() {
  const ref = useRef(null);
  // ... 使用ref.current
}

6. 使用场景与注意事项

典型场景

  • 访问DOM元素
    function Input() {
      const inputRef = useRef(null);
      useEffect(() => {
        inputRef.current.focus(); // 组件挂载后聚焦输入框
      }, []);
      return <input ref={inputRef} />;
    }
    
  • 保存可变值
    function Timer() {
      const intervalRef = useRef();
      useEffect(() => {
        intervalRef.current = setInterval(() => {
          // 执行定时任务
        }, 1000);
        return () => clearInterval(intervalRef.current);
      }, []);
    }
    

注意事项

  • 不要在渲染期间修改ref.current:这可能导致不可预测的行为,应在useEffect或事件处理中修改。
  • 避免滥用useRef:如果数据需要触发渲染,应使用useStateuseReducer

通过这种设计,useRef在React中实现了轻量级、高性能的持久化引用管理,同时与React的渲染机制无缝集成。