魯斯前端布魯斯前端

文章中英模式

布魯斯前端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)的更新請求