Vue 中 ref 的作用
Vue 裡的 ref 有兩個常見語境,很容易被混在一起:
- 響應式 ref:用來建立一個響應式資料容器,例如
const count = ref(0)。 - 模板 ref:用來取得 DOM 元素或子元件實例,例如
<input ref="inputRef" />。
一句話回答:ref 在 Composition API 中主要用來包裝一個可響應的值,透過 .value 讀寫;在模板中,ref 也可以作為特殊 attribute,用來取得 DOM 或子元件引用。
一、 必考觀念
1. 響應式 ref 是什麼?
ref 可以把一個值包成響應式容器:
import { ref } from 'vue'
const count = ref(0)
count.value++它回傳的不是原始數字,而是一個 ref object:
{
value: 0
}Vue 會攔截 .value 的 get/set:
- 讀
.value時收集依賴。 - 寫
.value時觸發更新。
簡化心智模型:
function ref(rawValue) {
return {
get value() {
track()
return rawValue
},
set value(nextValue) {
rawValue = nextValue
trigger()
},
}
}真實實作更複雜,還會處理物件轉 reactive、shallow、依賴集合等。
2. 為什麼需要 .value?
JavaScript 的原始型別不是引用,無法直接攔截普通變數的讀寫。
let count = 0
count++Vue 無法知道你什麼時候讀了 count、什麼時候改了 count。
所以 Vue 用一個物件包住它:
const count = {
value: 0,
}物件屬性可以透過 getter/setter 攔截,這就是 .value 的必要性。
3. 模板中為什麼不用 .value?
在 template 裡,Vue 會自動解包 ref。
<template>
<button @click="count++">
{{ count }}
</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>模板裡可以直接寫 count,不需要 count.value。
但在 <script setup> 裡仍要:
count.value++這是很多新手容易混淆的地方。
二、 ref 可以包什麼?
1. 包原始型別
const count = ref(0)
const name = ref('Ada')
const visible = ref(false)這是 ref 最常見用途。
2. 包物件
const user = ref({
name: 'Ada',
age: 28,
})Vue 會把物件值轉成 reactive,所以內部屬性也能被追蹤:
user.value.name = 'Grace'但注意,外層仍然要 .value。
3. 替換整個值
ref 很適合需要整體替換的資料:
const user = ref(null)
user.value = await fetchUser()這也是 ref 和 reactive 的重要差異之一:ref 天然適合整體替換。
三、 模板 ref
1. 取得 DOM 元素
<template>
<input ref="inputRef" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>這裡的 ref="inputRef" 是模板 ref。Vue 會在 DOM 掛載後,把真實 input 元素放進 inputRef.value。
2. 配合 nextTick
如果 DOM 是由 v-if 控制,狀態改變後要等 DOM 更新完成才能拿到 ref:
<template>
<button @click="open">Open</button>
<input v-if="visible" ref="inputRef" />
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const visible = ref(false)
const inputRef = ref<HTMLInputElement | null>(null)
async function open() {
visible.value = true
await nextTick()
inputRef.value?.focus()
}
</script>延伸讀:nextTick 的作用與實現原理。
3. 取得子元件實例
模板 ref 也可以指向子元件:
<template>
<BaseInput ref="baseInputRef" />
</template>在 <script setup> 子元件裡,預設不會暴露所有內部內容。需要用 defineExpose:
<!-- BaseInput.vue -->
<script setup lang="ts">
function focus() {
// focus internal input
}
defineExpose({
focus,
})
</script>父元件:
const baseInputRef = ref<InstanceType<typeof BaseInput> | null>(null)
baseInputRef.value?.focus()這屬於命令式通訊,適合 focus、reset、scroll 這類操作,不適合一般資料流。
四、 ref 與 reactive 的差異
| 比較 | ref | reactive |
|---|---|---|
| 適合資料 | 原始型別、可整體替換的值 | 物件、表單狀態、結構化狀態 |
| 存取方式 | script 中要 .value | 直接讀寫屬性 |
| 替換整體 | 很自然 | 直接替換會失去原 reactive 引用 |
| 解構 | 可用 toRefs 或保持 ref | 直接解構會丟失響應性 |
1. ref 適合原始值
const count = ref(0)
const keyword = ref('')
const loading = ref(false)2. reactive 適合物件狀態
const form = reactive({
email: '',
password: '',
})但如果你要整個替換:
// 不建議:外部仍指向舊 reactive object
form = newForm這時 ref 包物件更自然:
const form = ref({
email: '',
password: '',
})
form.value = newForm五、 ref 解包規則
1. 模板自動解包
<template>
{{ count }}
</template>等價於讀 count.value。
2. reactive 物件中的 ref 會解包
const count = ref(0)
const state = reactive({
count,
})
console.log(state.count) // 0讀 state.count 時會自動解包。
但陣列和集合裡的 ref 不會像物件屬性那樣完全解包:
const list = reactive([ref(1)])
console.log(list[0].value)這是面試常見細節,不必一開始背很深,但要知道 ref 解包不是所有場景都一樣。
3. 解構 reactive 會丟失響應性
const state = reactive({
count: 0,
})
const { count } = state這裡的 count 是普通值,不再響應。
可以用 toRefs:
const { count } = toRefs(state)
count.value++六、 shallowRef
shallowRef 只追蹤 .value 本身,不深層轉換物件。
const chart = shallowRef(null)適合:
- 第三方實例。
- 大型不可變資料。
- 不希望 Vue 深層代理的物件。
- 手動控制更新的資料。
例如圖表實例:
const chartInstance = shallowRef(null)你通常只關心整個 instance 是否替換,不需要 Vue 追蹤 instance 裡面每個屬性。
七、 追問題庫
Q1:ref 的作用是什麼?
有兩種常見答案:
- 在 Composition API 中建立響應式資料。
- 在模板中取得 DOM 或子元件引用。
面試時最好主動區分這兩種語境。
Q2:為什麼 script 裡要 .value?
因為 ref 是一個物件容器,真正的值在 .value 上。Vue 透過 .value 的 getter/setter 做依賴收集與觸發更新。
Q3:ref 和 reactive 怎麼選?
簡單策略:
- 原始型別用
ref。 - 需要整體替換用
ref。 - 結構化物件狀態可用
reactive。 - composable 對外返回狀態時,常用 ref 或
toRefs保持解構友好。
Q4:template ref 什麼時候有值?
DOM 掛載後才有值。
在 onMounted 裡通常可以取得。如果元素受 v-if 控制,切換後要等 nextTick。
Q5:可以用 ref 做元件通訊嗎?
可以,但它是命令式方式,耦合較高。
適合 focus、reset、scrollTo 這類命令,不適合一般資料同步。一般資料流應該優先使用 Props、Emit、v-model、Provide/Inject 或 Pinia。
延伸讀:Vue 元件通訊方式有哪些及原理。
八、 實作題
題目:打開搜尋框後自動 focus
需求:
- 點擊按鈕顯示搜尋框。
- 搜尋框顯示後自動 focus。
<template>
<button @click="openSearch">Search</button>
<input
v-if="visible"
ref="inputRef"
v-model="keyword"
/>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const visible = ref(false)
const keyword = ref('')
const inputRef = ref<HTMLInputElement | null>(null)
async function openSearch() {
visible.value = true
await nextTick()
inputRef.value?.focus()
}
</script>這裡有三種 ref:
visible:響應式布林值。keyword:響應式字串,配合v-model。inputRef:模板 ref,指向 DOM input。
九、 資深視角
1. ref 是響應式邊界
ref 把普通值包成可追蹤的響應式邊界。這讓原始值也能參與 Vue 的依賴追蹤。
從系統設計角度看,ref 是一個明確的「可變狀態容器」。
2. template ref 是逃生口
大部分 Vue 邏輯應該資料驅動,但有些場景必須碰 DOM:
- focus。
- 選取文字。
- 讀尺寸。
- scroll。
- 接第三方套件。
template ref 是 Vue 給你的 escape hatch。它很有用,但不應該取代正常資料流。
3. 大型資料要考慮 shallowRef
如果你把大型資料或第三方 class instance 放進普通 ref,Vue 可能會深層轉 reactive。
這不一定是你想要的。對不可變資料、第三方實例、大型表格資料,可以評估 shallowRef、markRaw 或資料切片。
總結
| 問題 | 面試回答重點 |
|---|---|
| ref 的作用 | 建立響應式值;也可作為模板引用 |
為什麼有 .value | 透過物件 getter/setter 追蹤與觸發更新 |
模板是否要 .value | 不需要,模板會自動解包 |
| ref vs reactive | ref 適合原始值與整體替換,reactive 適合物件狀態 |
| template ref 何時有值 | DOM 掛載後;v-if 切換後通常要 nextTick |
| ref 通訊風險 | 命令式耦合高,不應取代 Props/Emit |
一句完整的面試回答可以是:
Vue 中的
ref有兩種常見作用。第一,它可以在 Composition API 中建立響應式資料容器,真正的值放在.value上,Vue 透過.value的 getter/setter 做依賴收集和更新觸發;模板中會自動解包,所以不需要寫.value。第二,ref也可以作為模板引用,用來取得 DOM 元素或子元件實例,常用於 focus、scroll、reset 等命令式操作。實務上要區分響應式 ref 和 template ref,不要把 template ref 當成一般資料流工具。