React useRef 源码解读
概述
useRef 是 React Hooks 中一个看似简单却非常实用的 Hook。它主要用于:
- 获取 DOM 元素的引用
- 在组件多次渲染之间保持可变值,且修改不会触发重新渲染
本文将深入源码,解析 useRef 的实现原理。
基本用法回顾
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
源码位置
useRef 的源码主要位于 React 仓库的以下文件:
packages/react/src/ReactHooks.js- Hook 的入口定义packages/react-reconciler/src/ReactFiberHooks.js- 实际实现逻辑
源码解析
1. Hook 入口定义
在 ReactHooks.js 中,useRef 只是一个简单的委托:
export function useRef<T>(initialValue: T): {current: T} {
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
这里通过 resolveDispatcher() 获取当前的 dispatcher,根据组件所处的阶段(mount/update),dispatcher 会指向不同的实现。
2. Mount 阶段 - mountRef
首次渲染时调用 mountRef:
function mountRef<T>(initialValue: T): {current: T} {
// 获取当前正在工作的 Hook
const hook = mountWorkInProgressHook();
// 创建 ref 对象
const ref = {current: initialValue};
// 将 ref 对象存储在 hook.memoizedState 中
hook.memoizedState = ref;
return ref;
}
核心逻辑:
- 创建一个包含
current属性的普通对象 - 将这个对象保存在 Hook 的
memoizedState中 - 返回这个 ref 对象
这就是为什么 useRef 返回的对象在整个组件生命周期中保持同一引用的原因。
3. Update 阶段 - updateRef
组件更新时调用 updateRef:
function updateRef<T>(initialValue: T): {current: T} {
// 获取当前 Hook
const hook = updateWorkInProgressHook();
// 直接返回之前保存的 ref 对象
return hook.memoizedState;
}
核心逻辑:
- 直接返回 mount 阶段创建并保存的 ref 对象
- 注意:这里完全忽略了传入的
initialValue,因为初始值只在 mount 时使用
4. Hook 链表结构
每个组件的 Hooks 通过链表连接:
type Hook = {
memoizedState: any, // 保存 Hook 的状态(对于 useRef 就是 ref 对象)
baseState: any,
baseQueue: Update<any> | null,
queue: UpdateQueue<any> | null,
next: Hook | null, // 指向下一个 Hook
};
useRef 只使用了 memoizedState 和 next 字段。
与 useState 的对比
理解 useRef 和 useState 的区别很重要:
// useState 的简化实现
function mountState(initialState) {
const hook = mountWorkInProgressHook();
hook.memoizedState = initialState;
const dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue);
return [hook.memoizedState, dispatch];
}
// useRef 的简化实现
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
关键区别:
- useState 保存的是值本身,修改需要调用 setter,会触发重新渲染
- useRef 保存的是对象引用,可以直接修改
current属性,不会触发重新渲染
为什么修改 ref.current 不会触发重新渲染?
const ref = useRef(0);
// 这样修改不会触发重新渲染
ref.current = ref.current + 1;
原因在于:
- React 的更新机制依赖于调用特定的更新函数(如
setState) useRef返回的是一个普通 JavaScript 对象- 直接修改对象属性不会被 React 的调度系统感知
- 没有触发
scheduleUpdateOnFiber,自然不会重新渲染
实际应用场景
1. 保存 DOM 引用
function VideoPlayer() {
const videoRef = useRef(null);
const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();
return <video ref={videoRef} />;
}
2. 保存前一次的值
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <div>Now: {count}, Before: {prevCount}</div>;
}
3. 保存定时器 ID
function Timer() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
}
4. 避免闭包陷阱
function Chat() {
const [text, setText] = useState('');
const textRef = useRef(text);
useEffect(() => {
textRef.current = text;
}, [text]);
const handleSend = useCallback(() => {
// 总是能获取到最新的 text 值
alert(textRef.current);
}, []); // 依赖数组为空,但仍能访问最新值
}
性能优化技巧
1. useRef vs createRef
// ❌ 每次渲染都创建新的 ref 对象
function BadExample() {
const ref = createRef();
return <div ref={ref} />;
}
// ✅ 整个生命周期使用同一个 ref 对象
function GoodExample() {
const ref = useRef();
return <div ref={ref} />;
}
2. 惰性初始化
虽然 useRef 没有像 useState 那样的函数式初始化,但你可以这样做:
function ExpensiveComponent() {
const expensiveRef = useRef(null);
if (expensiveRef.current === null) {
expensiveRef.current = createExpensiveObject();
}
return <div>{expensiveRef.current.value}</div>;
}
源码中的注意事项
1. 初始值只在 mount 时使用
// ⚠️ 更新时传入新的初始值不会生效
function Example({ newValue }) {
const ref = useRef(newValue); // newValue 变化不会更新 ref
// 如果需要同步,必须手动赋值
useEffect(() => {
ref.current = newValue;
}, [newValue]);
}
2. ref 对象的不可变性
React 源码中创建的 ref 对象是密封的(在开发模式下):
if (__DEV__) {
Object.seal(ref);
}
这意味着你不能添加或删除 current 以外的属性。
总结
useRef 的实现非常简洁:
- Mount 阶段:创建
{current: initialValue}对象并保存 - Update 阶段:返回保存的同一个对象
- 核心特性:对象引用不变,修改
current不触发渲染
这种简单的设计使得 useRef 成为 React Hooks 中最容易理解但又非常强大的工具之一。
相关源码链接
版本说明: 本文基于 React 18+ 源码分析,不同版本可能存在细微差异。