響應式基礎
API 參考
本頁和後面很多頁面中都分別包含了選項式 API 和組合式 API 的示例代碼。現在你選擇的是 組合式 API。你可以使用左側側邊欄頂部的“API 風格偏好”開關在 API 風格之間切換。
聲明響應式狀態
ref()
在組合式 API 中,推薦使用 ref()
函數來聲明響應式狀態:
js
import { ref } from 'vue'
const count = ref(0)
ref()
接收參數,並將其包裹在一個帶有 .value
屬性的 ref 對象中返回:
js
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
參考:為 refs 標註類型
要在組件模板中訪問 ref,請從組件的 setup()
函數中聲明並返回它們:
js
import { ref } from 'vue'
export default {
// `setup` 是一個特殊的鉤子,專門用於組合式 API。
setup() {
const count = ref(0)
// 將 ref 暴露給模板
return {
count
}
}
}
template
<div>{{ count }}</div>
注意,在模板中使用 ref 時,我們不需要附加 .value
。為了方便起見,當在模板中使用時,ref 會自動解包 (有一些注意事項)。
你也可以直接在事件監聽器中改變一個 ref:
template
<button @click="count++">
{{ count }}
</button>
對於更復雜的邏輯,我們可以在同一作用域內聲明更改 ref 的函數,並將它們作為方法與狀態一起公開:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// 在 JavaScript 中需要 .value
count.value++
}
// 不要忘記同時暴露 increment 函數
return {
count,
increment
}
}
}
然後,暴露的方法可以被用作事件監聽器:
template
<button @click="increment">
{{ count }}
</button>
這裡是 Codepen 上的例子,沒有使用任何構建工具。
<script setup>
在 setup()
函數中手動暴露大量的狀態和方法非常繁瑣。幸運的是,我們可以通過使用單文件組件 (SFC) 來避免這種情況。我們可以使用 <script setup>
來大幅度地簡化代碼:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
<script setup>
中的頂層的導入、聲明的變量和函數可在同一組件的模板中直接使用。你可以理解為模板是在同一作用域內聲明的一個 JavaScript 函數——它自然可以訪問與它一起聲明的所有內容。
TIP
在指南的後續章節中,我們基本上都會在組合式 API 示例中使用單文件組件 + <script setup>
的語法,因為大多數 Vue 開發者都會這樣使用。
如果你沒有使用單文件組件,你仍然可以在 setup()
選項中使用組合式 API。
為什麼要使用 ref?
你可能會好奇:為什麼我們需要使用帶有 .value
的 ref,而不是普通的變量?為了解釋這一點,我們需要簡單地討論一下 Vue 的響應式系統是如何工作的。
當你在模板中使用了一個 ref,然後改變了這個 ref 的值時,Vue 會自動檢測到這個變化,並且相應地更新 DOM。這是通過一個基於依賴追蹤的響應式系統實現的。當一個組件首次渲染時,Vue 會追蹤在渲染過程中使用的每一個 ref。然後,當一個 ref 被修改時,它會觸發追蹤它的組件的一次重新渲染。
在標準的 JavaScript 中,檢測普通變量的訪問或修改是行不通的。然而,我們可以通過 getter 和 setter 方法來攔截對象屬性的 get 和 set 操作。
該 .value
屬性給予了 Vue 一個機會來檢測 ref 何時被訪問或修改。在其內部,Vue 在它的 getter 中執行追蹤,在它的 setter 中執行觸發。從概念上講,你可以將 ref 看作是一個像這樣的對象:
js
// 偽代碼,並非真正的實現
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
另一個 ref 的好處是,與普通變量不同,你可以將 ref 傳遞給函數,同時保留對最新值和響應式連接的訪問。當將複雜的邏輯重構為可重用的代碼時,這將非常有用。
該響應性系統在深入響應式原理章節中有更詳細的討論。
深層響應性
Ref 可以持有任何類型的值,包括深層嵌套的對象、數組或者 JavaScript 內置的數據結構,例如 Map
。
Ref 會使它的值具有深層響應性。這意味著即使改變嵌套對象或數組時,變化也會被檢測到:
js
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都會按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
非原始值將通過 reactive()
轉換為響應式代理,該函數將在後面討論。
也可以通過 shallow ref 來放棄深層響應性。對於淺層 ref,只有 .value
的訪問會被追蹤。淺層 ref 可以用於避免對大型數據的響應性開銷來優化性能、或者有外部庫管理其內部狀態的情況。
閱讀更多:
DOM 更新時機
當你修改了響應式狀態時,DOM 會被自動更新。但是需要注意的是,DOM 更新不是同步的。Vue 會在“next tick”更新週期中緩衝所有狀態的修改,以確保不管你進行了多少次狀態修改,每個組件都只會被更新一次。
要等待 DOM 更新完成後再執行額外的代碼,可以使用 nextTick() 全局 API:
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 現在 DOM 已經更新了
}
reactive()
還有另一種聲明響應式狀態的方式,即使用 reactive()
API。與將內部值包裝在特殊對象中的 ref 不同,reactive()
將使對象本身具有響應性:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
在模板中使用:
template
<button @click="state.count++">
{{ state.count }}
</button>
響應式對象是 JavaScript 代理,其行為就和普通對象一樣。不同的是,Vue 能夠攔截對響應式對象所有屬性的訪問和修改,以便進行依賴追蹤和觸發更新。
reactive()
將深層地轉換對象:當訪問嵌套對象時,它們也會被 reactive()
包裝。當 ref 的值是一個對象時,ref()
也會在內部調用它。與淺層 ref 類似,這裡也有一個 shallowReactive()
API 可以選擇退出深層響應性。
Reactive Proxy vs. Original
值得注意的是,reactive()
返回的是一個原始對象的 Proxy,它和原始對象是不相等的:
js
const raw = {}
const proxy = reactive(raw)
// 代理對象和原始對象不是全等的
console.log(proxy === raw) // false
只有代理對象是響應式的,更改原始對象不會觸發更新。因此,使用 Vue 的響應式系統的最佳實踐是僅使用你聲明對象的代理版本。
為保證訪問代理的一致性,對同一個原始對象調用 reactive()
會總是返回同樣的代理對象,而對一個已存在的代理對象調用 reactive()
會返回其本身:
js
// 在同一個對象上調用 reactive() 會返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一個代理上調用 reactive() 會返回它自己
console.log(reactive(proxy) === proxy) // true
這個規則對嵌套對象也適用。依靠深層響應性,響應式對象內的嵌套對象依然是代理:
js
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive()
的局限性
reactive()
API 有一些局限性:
有限的值類型:它只能用於對象類型 (對象、數組和如
Map
、Set
這樣的集合類型)。它不能持有如string
、number
或boolean
這樣的原始類型。不能替換整個對象:由於 Vue 的響應式跟蹤是通過屬性訪問實現的,因此我們必須始終保持對響應式對象的相同引用。這意味著我們不能輕易地“替換”響應式對象,因為這樣的話與第一個引用的響應性連接將丟失:
jslet state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用將不再被追蹤 // (響應性連接已丟失!) state = reactive({ count: 1 })
對解構操作不友好:當我們將響應式對象的原始類型屬性解構為本地變量時,或者將該屬性傳遞給函數時,我們將丟失響應性連接:
jsconst state = reactive({ count: 0 }) // 當解構時,count 已經與 state.count 斷開連接 let { count } = state // 不會影響原始的 state count++ // 該函數接收到的是一個普通的數字 // 並且無法追蹤 state.count 的變化 // 我們必須傳入整個對象以保持響應性 callSomeFunction(state.count)
由於這些限制,我們建議使用 ref()
作為聲明響應式狀態的主要 API。
額外的 ref 解包細節
作為 reactive 對象的屬性
一個 ref 會在作為響應式對象的屬性被訪問或修改時自動解包。換句話說,它的行為就像一個普通的屬性:
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果將一個新的 ref 賦值給一個關聯了已有 ref 的屬性,那麼它會替換掉舊的 ref:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 現在已經和 state.count 失去聯繫
console.log(count.value) // 1
只有當嵌套在一個深層響應式對象內時,才會發生 ref 解包。當其作為淺層響應式對象的屬性被訪問時不會解包。
數組和集合的注意事項
與 reactive 對象不同的是,當 ref 作為響應式數組或原生集合類型 (如 Map
) 中的元素被訪問時,它不會被解包:
js
const books = reactive([ref('Vue 3 Guide')])
// 這裡需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 這裡需要 .value
console.log(map.get('count').value)
在模板中解包的注意事項
在模板渲染上下文中,只有頂級的 ref 屬性才會被解包。
在下面的例子中,count
和 object
是頂級屬性,但 object.id
不是:
js
const count = ref(0)
const object = { id: ref(1) }
因此,這個表達式按預期工作:
template
{{ count + 1 }}
...但這個不會:
template
{{ object.id + 1 }}
渲染的結果將是 [object Object]1
,因為在計算表達式時 object.id
沒有被解包,仍然是一個 ref 對象。為了解決這個問題,我們可以將 id
解構為一個頂級屬性:
js
const { id } = object
template
{{ id + 1 }}
現在渲染的結果將是 2
。
另一個需要注意的點是,如果 ref 是文本插值的最終計算值 (即 {{ }}
標籤),那麼它將被解包,因此以下內容將渲染為 1
:
template
{{ object.id }}
該特性僅僅是文本插值的一個便利特性,等價於 {{ object.id.value }}
。