用ai去看源码

React useRef 源码解读

概述

useRef 是 React Hooks 中一个看似简单却非常实用的 Hook。它主要用于:

  1. 获取 DOM 元素的引用
  2. 在组件多次渲染之间保持可变值,且修改不会触发重新渲染

本文将深入源码,解析 useRef 的实现原理。

基本用法回顾

javascript 复制代码
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 只是一个简单的委托:

javascript 复制代码
export function useRef<T>(initialValue: T): {current: T} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

这里通过 resolveDispatcher() 获取当前的 dispatcher,根据组件所处的阶段(mount/update),dispatcher 会指向不同的实现。

2. Mount 阶段 - mountRef

首次渲染时调用 mountRef

javascript 复制代码
function mountRef<T>(initialValue: T): {current: T} {
  // 获取当前正在工作的 Hook
  const hook = mountWorkInProgressHook();
  
  // 创建 ref 对象
  const ref = {current: initialValue};
  
  // 将 ref 对象存储在 hook.memoizedState 中
  hook.memoizedState = ref;
  
  return ref;
}

核心逻辑:

  1. 创建一个包含 current 属性的普通对象
  2. 将这个对象保存在 Hook 的 memoizedState
  3. 返回这个 ref 对象

这就是为什么 useRef 返回的对象在整个组件生命周期中保持同一引用的原因。

3. Update 阶段 - updateRef

组件更新时调用 updateRef

javascript 复制代码
function updateRef<T>(initialValue: T): {current: T} {
  // 获取当前 Hook
  const hook = updateWorkInProgressHook();
  
  // 直接返回之前保存的 ref 对象
  return hook.memoizedState;
}

核心逻辑:

  • 直接返回 mount 阶段创建并保存的 ref 对象
  • 注意:这里完全忽略了传入的 initialValue,因为初始值只在 mount 时使用

4. Hook 链表结构

每个组件的 Hooks 通过链表连接:

javascript 复制代码
type Hook = {
  memoizedState: any,      // 保存 Hook 的状态(对于 useRef 就是 ref 对象)
  baseState: any,
  baseQueue: Update<any> | null,
  queue: UpdateQueue<any> | null,
  next: Hook | null,       // 指向下一个 Hook
};

useRef 只使用了 memoizedStatenext 字段。

与 useState 的对比

理解 useRefuseState 的区别很重要:

javascript 复制代码
// 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;
}

关键区别:

  1. useState 保存的是值本身,修改需要调用 setter,会触发重新渲染
  2. useRef 保存的是对象引用,可以直接修改 current 属性,不会触发重新渲染

为什么修改 ref.current 不会触发重新渲染?

javascript 复制代码
const ref = useRef(0);

// 这样修改不会触发重新渲染
ref.current = ref.current + 1;

原因在于:

  1. React 的更新机制依赖于调用特定的更新函数(如 setState
  2. useRef 返回的是一个普通 JavaScript 对象
  3. 直接修改对象属性不会被 React 的调度系统感知
  4. 没有触发 scheduleUpdateOnFiber,自然不会重新渲染

实际应用场景

1. 保存 DOM 引用

javascript 复制代码
function VideoPlayer() {
  const videoRef = useRef(null);
  
  const play = () => videoRef.current.play();
  const pause = () => videoRef.current.pause();
  
  return <video ref={videoRef} />;
}

2. 保存前一次的值

javascript 复制代码
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

javascript 复制代码
function Timer() {
  const intervalRef = useRef(null);
  
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
    
    return () => clearInterval(intervalRef.current);
  }, []);
}

4. 避免闭包陷阱

javascript 复制代码
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

javascript 复制代码
// ❌ 每次渲染都创建新的 ref 对象
function BadExample() {
  const ref = createRef();
  return <div ref={ref} />;
}

// ✅ 整个生命周期使用同一个 ref 对象
function GoodExample() {
  const ref = useRef();
  return <div ref={ref} />;
}

2. 惰性初始化

虽然 useRef 没有像 useState 那样的函数式初始化,但你可以这样做:

javascript 复制代码
function ExpensiveComponent() {
  const expensiveRef = useRef(null);
  
  if (expensiveRef.current === null) {
    expensiveRef.current = createExpensiveObject();
  }
  
  return <div>{expensiveRef.current.value}</div>;
}

源码中的注意事项

1. 初始值只在 mount 时使用

javascript 复制代码
// ⚠️ 更新时传入新的初始值不会生效
function Example({ newValue }) {
  const ref = useRef(newValue); // newValue 变化不会更新 ref
  
  // 如果需要同步,必须手动赋值
  useEffect(() => {
    ref.current = newValue;
  }, [newValue]);
}

2. ref 对象的不可变性

React 源码中创建的 ref 对象是密封的(在开发模式下):

javascript 复制代码
if (__DEV__) {
  Object.seal(ref);
}

这意味着你不能添加或删除 current 以外的属性。

总结

useRef 的实现非常简洁:

  1. Mount 阶段 :创建 {current: initialValue} 对象并保存
  2. Update 阶段:返回保存的同一个对象
  3. 核心特性 :对象引用不变,修改 current 不触发渲染

这种简单的设计使得 useRef 成为 React Hooks 中最容易理解但又非常强大的工具之一。

相关源码链接


版本说明: 本文基于 React 18+ 源码分析,不同版本可能存在细微差异。

相关推荐
踩着两条虫1 小时前
AI 驱动的 Vue3 应用开发平台 深入探究(六):双向代码转换之DSL到Vue代码生成
前端·vue.js·ai编程
前端老兵AI1 小时前
React vs Vue 2026年怎么选?9年前端的真实建议
vue.js·react.js
Wect1 小时前
React 中的双缓存 Fiber 树机制
前端·react.js·面试
天才熊猫君1 小时前
Vue 3 中 Watch 的陷阱:为什么异步操作后创建的监听会泄漏?
前端·javascript
梵得儿SHI1 小时前
Vue3 生态工具实战进阶:API 请求封装 + 样式解决方案全攻略(Axios/Sass/CSS Modules)
前端·css·vue3·sass·api请求·样式解决方案·组合式api管理
有梦想的咸鱼还是咸鱼吗1 小时前
前端必会|防抖与节流从原理到实战,解决90%高频事件卡顿问题
前端
王金涛1 小时前
三行代码,让你的 React 项目优雅地支持 Undo/Redo
react.js
阿诺木1 小时前
Node.js 局域网设备发现:mDNS、UDP 广播和子网扫描
前端
盐焗乳鸽还要砂锅1 小时前
亲手造一只有灵魂的 AI 小龙虾是种什么体验?
前端·llm·agent