文章中英模式
布魯斯前端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); // trueuseState 每次渲染產生新值
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. 建立一個具有current屬性的普通JavaScript物件
- 2. 確保這個物件在不同渲染之間保持穩定的引用
- 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:何時使用哪一個?
主要區別
| 特性 | useState | useRef |
|---|---|---|
| 更新時觸發重新渲染 | 是 ✅ | 否 ❌ |
| 值的存取方式 | 直接使用變數 | 通過.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. 建立一個具有
current屬性的普通 JavaScript 物件 - 2. 確保這個物件在不同渲染之間保持穩定的引用
- 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)的更新請求