在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的调用顺序,通过链表指针找到对应的
useRefHook节点。
步骤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的区别
| 特性 | useRef | useState |
|---|---|---|
| 触发渲染 | 修改.current不触发重新渲染 | 修改状态触发重新渲染 |
| 存储位置 | 数据保存在Hook的memoizedState | 状态保存在Hook的memoizedState和queue中 |
| 用途 | 保存与渲染无关的引用(如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:如果数据需要触发渲染,应使用useState或useReducer。
通过这种设计,useRef在React中实现了轻量级、高性能的持久化引用管理,同时与React的渲染机制无缝集成。