鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端React面试题目 - useRef用途与实现原理

深入了解React useRef的多种用途、内部实现原理与使用技巧。掌握如何使用useRef存取DOM元素、保存变量而不触发重新渲染,理解其与useState的区别,以及在面试中常见的useRef相关问题与解答。

影片縮圖

懒得看文章?那就来看视频吧

useRef的基本介绍与实用性

useRef 是 React 提供的一個 Hook,用於創建一個可以在組件生命週期內持續存在的可變引用。它最大的特點是:當 ref 的內容改變時,不會觸發組件重新渲染

渲染周期中的useRef vs useState

状态变化与渲染关系:

useRef

渲染1: ref.current = 0

修改值: ref.current = 1

→ 不触发重新渲染

渲染仍为1: ref.current = 1

useState

渲染1: [value=0, setValue]

修改值: setValue(1)

→ 触发重新渲染

渲染2: [value=1, setValue]

引用一致性比较:

useRef 保持同一引用

useRef 在組件的整個生命週期中返回同一個引用對象。即使組件重新渲染,ref 對象的身份也保持不變,只有 current 屬性的值可能改變。

// 首次渲染
const ref = useRef(0);  // ref = { current: 0 }

// 多次渲染後,ref仍是同一個對象
console.log(ref === 上次渲染的ref);  // true

useState 每次渲染产生新值

useState 在每次渲染時會產生當前狀態的新快照。雖然狀態值可能相同,但每次渲染中的狀態變量都是獨立的常量。

// 渲染1
const [count, setCount] = useState(0);
// 這個渲染中的count是0的快照

// 渲染2 (after setCount(1))
const [count, setCount] = useState(0);
// 這個渲染中的count是1的新快照

实际案例:计数器但不重新渲染

function SilentCounter() {
  const [, forceUpdate] = useState({});
  const countRef = useRef(0);
  
  return (
    <>
      <p>計數: {countRef.current}</p>
      <button onClick={() => {
        // 增加計數但不重新渲染
        countRef.current += 1;
        console.log("計數已增加到:", countRef.current);
      }}>
        增加計數 (不更新UI)
      </button>
      <button onClick={() => forceUpdate({})}>
        手動更新UI
      </button>
    </>
  );
}

useRef的三大主要用途

1. 存取DOM元素

这是useRef最常见的用途,直接访问DOM节点来执行命令式操作(如:聚焦、播放视频、计算尺寸)。

  • 1. 需要操作DOM元素
  • 2. 存储不影响UI的数据(计时器ID等)
  • 3. 需要记住值但不想触发重新渲染
  1. 1. 建立一个具有current属性的普通JavaScript对象
  2. 2. 确保这个对象在不同渲染之间保持稳定的引用
  3. 3. 不提供任何主动通知机制当ref内容变更时
function AutoPlayVideo() {
  const videoRef = useRef(null);
  
  useEffect(() => {
    // 元件掛載後自動播放
    videoRef.current.play();
    
    return () => {
      // 元件卸載前暫停
      videoRef.current.pause();
    };
  }, []);
  
  return <video ref={videoRef} src="https://example.com/video.mp4" />;
}

2. 保存可变的值(不触发重新渲染)

useRef可以存储任何类型的值,且在更新时不会触发重新渲染,适合保存那些不需要参与渲染计算的数据。

function IntervalCounter() {
  const [count, setCount] = useState(0);
  // 保存interval ID,便於清理
  const intervalRef = useRef(null);
  // 記錄最後一次渲染時的count值
  const previousCountRef = useRef(0);
  
  useEffect(() => {
    // 保存前一次的count(會在每次渲染後執行)
    previousCountRef.current = count;
    
    // 設置interval
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    return () => {
      // 清理interval
      clearInterval(intervalRef.current);
    };
  }, []);
  
  return (
    <div>
      現在: {count}, 之前: {previousCountRef.current}
    </div>
  );
}

3. 缓存计算结果(类似memoization)

当需要存储昂贵计算的结果,但又不希望每次重新渲染都重新计算时,useRef可作为轻量级的替代方案。

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  // 用ref快取前一次搜尋結果
  const prevQueryRef = useRef('');
  const cachedResultsRef = useRef({});
  
  useEffect(() => {
    // 如果查詢條件沒變,或者我們已經有快取結果,直接使用
    if (query === prevQueryRef.current && cachedResultsRef.current[query]) {
      setResults(cachedResultsRef.current[query]);
      return;
    }
    
    // 否則執行搜尋,並儲存結果
    fetchSearchResults(query).then(data => {
      setResults(data);
      prevQueryRef.current = query;
      cachedResultsRef.current[query] = data;
    });
  }, [query]);
  
  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

useRef vs useState:何时使用哪一个?

主要区别

特性useStateuseRef
更新时触发重新渲染是 ✅否 ❌
值的存取方式直接使用变量通过.current属性
更新方式使用setter函数直接修改.current
重新渲染时保持值是 ✅是 ✅
适用场景需要显示在UI上的数据不需参与渲染的值,或DOM引用

选择指南

什么时候使用 useState

  • 1. 当数据变化需要立即反映在UI上
  • 2. 当需要引起组件重新渲染以更新视图
  • 3. 当该值会影响渲染结果或其他state/effect依赖

什么时候使用 useRef

  • 1. 存储不影响渲染输出的值(计时器ID、前一次的props等)
  • 2. 需要直接访问DOM元素
  • 3. 需要存储在组件生命周期内持续存在但不触发更新的值
  • 4. 优化性能,避免不必要的重新渲染
// 考慮這個計數器例子,兩種實現方式的區別
function CounterExample() {
  // 更新時會重新渲染
  const [stateCount, setStateCount] = useState(0);
  // 更新時不會重新渲染
  const refCount = useRef(0);
  
  return (
    <>
      <div>
        <h3>useState計數器: {stateCount}</h3>
        <button onClick={() => setStateCount(c => c + 1)}>
          增加state計數
        </button>
      </div>
      
      <div>
        <h3>useRef計數器: {refCount.current}</h3>
        <button 
          onClick={() => {
            refCount.current += 1;
            // 注意: 這裡需要手動強制重新渲染才能看到更新
            console.log('useRef值已更新,但UI不會自動更新');
          }}
        >
          增加ref計數 (不會顯示變化)
        </button>
      </div>
    </>
  );
}

useRef 的内部实现原理

理解useRef的实现可以让我们更好地把握其行为特性。在React内部,useRef的实现非常简单:

// React內部useRef的簡化實現
function useRef(initialValue) {
  const hookIndex = currentComponent.currentHookIndex++;

  if (!currentComponent.hooks[hookIndex]) {
    currentComponent.hooks[hookIndex] = { current: initialValue };
  }

  return currentComponent.hooks[hookIndex];
}

实际上,useRef实现的核心在于:

  1. 1. 建立一个具有 current 属性的普通 JavaScript 对象
  2. 2. 确保这个对象在不同渲染之间保持稳定的引用
  3. 3. 不提供任何主动通知机制当ref内容变更时

在React的Fiber架构中,useRef的状态也保存在Fiber节点的memoizedState中,作为Hook链表的一部分:

// 在React Fiber中的Hook結構
{
  memoizedState: {
    /* useState的Hook節點 */
    // ...
    next: {
      // useRef的Hook節點
      memoizedState: { current: initialValue },
      next: {
        /* 下一個Hook */
        // ...
      }
    }
  }
}

进阶技巧与模式

使用useRef保存上一次的props或state

在某些情景下,需要比较当前值与前一次的值,useRef是理想的工具:

function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    // 每次渲染後更新ref的值
    ref.current = value;
  }, [value]);
  
  // 返回前一次的值
  return ref.current;
}

function ProfileUpdater({ userId }) {
  const prevUserId = usePrevious(userId);
  
  useEffect(() => {
    // 只有當用戶切換時才執行
    if (prevUserId !== userId) {
      console.log(`用戶從 ${prevUserId} 切換到 ${userId}`);
      fetchUserData(userId);
    }
  }, [userId, prevUserId]);
  
  // 組件其餘部分
}

条件性DOM引用

当元素可能不存在或条件渲染时,需要小心处理refs:

function ConditionalRefExample({ showInput }) {
  const inputRef = useRef(null);
  
  // 與useLayoutEffect結合使用,確保DOM操作安全
  useLayoutEffect(() => {
    // 安全地檢查元素是否存在
    if (showInput && inputRef.current) {
      inputRef.current.focus();
    }
  }, [showInput]);
  
  return (
    <div>
      {showInput && (
        <input ref={inputRef} type="text" placeholder="我會自動聚焦" />
      )}
      {/* 沒有元素時ref.current為null */}
    </div>
  );
}

🔥 常见面试题目

(一) useRef是什么?它有哪些主要用途?

解答: useRef是React的一个Hook,创建一个「记忆盒子」,即使组件重新渲染,里面的东西也不会丢失。

useRef盒子

.current

修改不会触发重新渲染

主要用途:

  • 1. 抓取DOM元素(如:让输入框自动聚焦)
  • 2. 存储不需触发重新渲染的数据
  • 3. 记住前一个状态值
  • 4. 保存计时器ID等需跨渲染周期的值

(二) useRef与useState有什么区别?

useState

修改值 → 触发重新渲染

使用setter函数更新

直接访问: value

useRef

修改值 → 不触发重新渲染

直接修改.current

访问: ref.current

(三) 为什么修改useRef不会触发重新渲染?

解答: 这与React的设计有关:

React渲染机制简图

useState

通知React更新

触发重新渲染

useRef

静默更新.current

React不知道变化

简单来说:

  • 1. useState有「通知机制」,告诉React需要更新UI
  • 2. useRef只是个容器,修改时没有通知React
  • 3. React只关注通过正规渠道(如setState)的更新请求