在前端框架的開發中,「操作真實 DOM 節點」以及「在組件生命週期中保存資料」是兩個常見的需求。在 React 中,我們第一個想到的工具通常是 useRef。
但你是否曾思考過:為什麼 React 需要 useRef?為什麼在 Vue 或 Angular 中,好像不需要一個「一兼二職」的 API?
本文將從 底層渲染模型(Render Model) 與 記憶體配置(Memory Layout) 的角度切入,一次梳理 React useRef 的本質,以及它在 Vue 3 與 Angular 中對應的實踐方式。
要理解 useRef,必須先理解 React 函式元件(Function Component)的底層運作機制。
在 React 中,「組件重新渲染(Re-render)= 整個 JavaScript 函式從頭到尾重新執行一次」。
function Counter() {
// ⚠️ 每次 re-render,此函式內部的每一行程式碼都會重新執行!
let count = 0;
const [state, setState] = useState(0);
const handleClick = () => {
count += 1;
setState(state + 1); // 觸發 Re-render
};
return <button onClick={handleClick}>Count: {count}</button>;
}
當 setState 觸發重新渲染時:
Counter() 函式。let count = 0 被重新宣告。count = 1 隨即被垃圾回收(Garbage Collection),值瞬間歸零。如果把變數寫在函式外部(Global Scope),雖然記憶體位址固定,但會導致多個組件實體共享同一份資料,破壞組件獨立性。
useRef 就是 React 給出的「記憶體逃逸口」:
const timerRef = useRef(null);
{ current: initialValue }。Counter() 函式被重新執行多少次,useRef 永遠返回指向同一個記憶體位址的物件。ref.current = newValue 是原地的物件屬性修改(Mutable Modification),不會觸發 React 的 Engine 重繪 UI。React 的 useRef 承擔了兩項職責:
在 Vue 3 與 Angular 中,這兩項職責從設計哲學上就被明確拆開了。
import { useRef } from 'react';
function MyComponent() {
// 職責 1:綁定 DOM 節點
const inputRef = useRef(null);
// 職責 2:保存非渲染狀態資料
const clickCount = useRef(0);
const handleFocus = () => {
inputRef.current?.focus();
clickCount.current += 1; // 不會觸發 Re-render
};
return <input ref={inputRef} type="text" />;
}
Vue 3 Composition API 的核心哲學與 React 截然不同:<script setup> 區塊在組件生命週期中只會執行一次!後續畫面更新由細粒度的響應式系統(Proxy)精準切換。
setup 只執行一次,使用一般的 JavaScript let 變數即可。藉由閉包(Closure),該變數的記憶體位址在組件生命週期內永遠存在,且變更它不會觸發 UI 更新。useTemplateRef 專職處理 DOM 引用。<script setup>
import { useTemplateRef } from 'vue'
// 職責 1:綁定 DOM (Vue 3.5+ 專用 API)
const inputRef = useTemplateRef('my-input')
// 職責 2:保存非渲染狀態資料
// 依靠 setup 的單次執行與閉包,不需要任何特殊的 Ref Hook
let clickCount = 0
const handleFocus = () => {
inputRef.value?.focus()
clickCount += 1
}
</script>
<template>
<input ref="my-input" type="text" />
</template>
Angular 採用標準的 OOP 類別結構。當組件被實體化(new MyComponent())時,屬性天然附著在 this 實體上,保存在 Heap 記憶體中。
viewChild Signal API,或傳統的 @ViewChild 裝飾器。import { Component, ElementRef, viewChild } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `<input #myInput type="text" />`
})
export class MyComponent {
// 職責 1:綁定 DOM (Angular 17+ Signal 寫法)
inputRef = viewChild<ElementRef<HTMLInputElement>>('myInput');
// 職責 2:保存非渲染狀態資料(Class 私有屬性)
private clickCount = 0;
handleFocus() {
this.inputRef()?.nativeElement.focus();
this.clickCount += 1;
}
}
| 框架 | 渲染模型(Render Model) | 記憶體保存機制 | DOM 引用實踐 |
|---|---|---|---|
| React | 每次 Re-render 重新執行整個函式 | useRef(固定 Heap 記憶體指標的 { current } 物件) |
const el = useRef(null) |
| Vue 3 | Setup 僅執行 1 次,Proxy 觸發更新 | 依靠 setup() 閉包,普通 let 變數即可持久化 |
const el = useTemplateRef('id') |
| Angular | Class 實體化,依賴 Change Detection | 直接作為 Class Property 附著在 this 實體上 |
el = viewChild('id') |
隨著 React 的演進(React 19+ 與 React Compiler),useRef 過去承擔的部分負擔已被新機制分流:
forwardRef 走入歷史:ref 已被升格為普通 Prop,不再需要繁瑣的 forwardRef 包裹子組件。// React 19+:直接傳遞與接收 ref prop
function CustomInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
useEffect 搭配 useRef。<input ref={(node) => {
// 元素掛載
return () => { /* 元素卸載清理 */ };
}} />
useOptimistic 處理,表單與非同步狀態交給 useActionState,useRef 不再被濫用為狀態管理的「臨時避難所」。useRef** 是在「全函式執行」模型下,維護固定記憶體指標與存取 DOM 的必要逃逸機制。useTemplateRef 專職 DOM 操作。理解各框架在「渲染」與「記憶體」上的底層設計哲學,能讓我們在選擇工具時更加明晰,寫出更高效、優雅的代码。