響應式轉換
Composition-API-specific
響應式轉換是 Composition-API-specific 特性,需要構建步驟。
引用與響應式變數的比較
自從 Composition API 介紹以來,一個主要未解決的問題就是引用與響應式物件的使用。在解構響應式物件時,很容易丟失響應性,而當使用引用時,需要在每個地方使用 .value
,這可能很繁瑣。此外,如果不使用型別系統,很容易錯過 .value
。
Vue Reactivity Transform 是一個編譯時轉換,允許我們編寫如下程式碼
vue
<script setup>
let count = $ref(0)
console.log(count)
function increment() {
count++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
這裡的 $ref()
方法是一個 編譯時宏:它不是一個在執行時實際呼叫的方法。相反,Vue 編譯器將其用作提示,將生成的 count
變數視為一個 響應式變數。
響應式變數可以像普通變數一樣訪問和重新賦值,但這些操作會被編譯成帶有 .value
的引用。例如,上述元件的 <script>
部分被編譯成
js
import { ref } from 'vue'
let count = ref(0)
console.log(count.value)
function increment() {
count.value++
}
每個返回引用的響應式 API 都有一個以 $
為字首的宏等效項。這些 API 包括
ref
->$ref
computed
->$computed
shallowRef
->$shallowRef
customRef
->$customRef
toRef
->$toRef
這些宏在全域性範圍內可用,當啟用 Reactivity Transform 時不需要匯入,但您可以可選地從 vue/macros
匯入它們,如果您想更加明確的話。
js
import { $ref } from 'vue/macros'
let count = $ref(0)
使用 $()
進行解構
通常,組合函式會返回一個引用的物件,並使用解構來檢索這些引用。為此,響應式轉換提供了 $()
宏。
js
import { useMouse } from '@vueuse/core'
const { x, y } = $(useMouse())
console.log(x, y)
編譯輸出
js
import { toRef } from 'vue'
import { useMouse } from '@vueuse/core'
const __temp = useMouse(),
x = toRef(__temp, 'x'),
y = toRef(__temp, 'y')
console.log(x.value, y.value)
請注意,如果 x
已經是一個引用,則 toRef(__temp, 'x')
將直接返回它,而不會建立額外的引用。如果解構的值不是一個引用(例如函式),它仍然可以工作 - 值將被包裹在一個引用中,以便其餘程式碼按預期工作。
$()
解構在響應式物件 和 包含引用的普通物件上都有效。
使用 $()
將現有引用轉換為響應式變數
在某些情況下,我們可能已經包裝了返回引用的函式。然而,Vue 編譯器無法事先知道函式將返回一個引用。在這種情況下,也可以使用 $()
宏將任何現有引用轉換為響應式變數。
js
function myCreateRef() {
return ref(0)
}
let count = $(myCreateRef())
響應式屬性解構
在當前 defineProps()
在 <script setup>
中的使用中存在兩個痛點
類似於
.value
,您需要始終以props.x
的形式訪問屬性以保持響應性。這意味著您不能解構defineProps
,因為解構後的變數不是響應式的,並且不會更新。當使用 僅型別屬性宣告 時,沒有簡單的方法來宣告屬性的預設值。我們引入了
withDefaults()
API 用於此目的,但它仍然使用起來很繁瑣。
我們可以透過在解構時使用 defineProps
應用編譯時轉換來解決這些問題,類似於我們之前看到的 $()
。
html
<script setup lang="ts">
interface Props {
msg: string
count?: number
foo?: string
}
const {
msg,
// default value just works
count = 1,
// local aliasing also just works
// here we are aliasing `props.foo` to `bar`
foo: bar
} = defineProps<Props>()
watchEffect(() => {
// will log whenever the props change
console.log(msg, count, bar)
})
</script>
上面的程式碼將被編譯成以下執行時宣告等效
js
export default {
props: {
msg: { type: String, required: true },
count: { type: Number, default: 1 },
foo: String
},
setup(props) {
watchEffect(() => {
console.log(props.msg, props.count, props.foo)
})
}
}
在函式邊界間保持響應性
雖然響應式變數使我們不必在所有地方都使用 .value
,但當我們跨函式邊界傳遞響應式變數時,它會產生“響應性丟失”的問題。這發生在兩種情況下
作為引數傳遞到函式中
給定一個期望將引用作為引數的函式,例如。
ts
function trackChange(x: Ref<number>) {
watch(x, (x) => {
console.log('x changed!')
})
}
let count = $ref(0)
trackChange(count) // doesn't work!
上述情況將無法按預期工作,因為它編譯為
ts
let count = ref(0)
trackChange(count.value)
在這裡,count.value
被作為一個數字傳遞,而trackChange
期望一個實際的ref。可以透過在傳遞之前用$$()
包裹count
來修復這個問題
diff
let count = $ref(0)
- trackChange(count)
+ trackChange($$(count))
上述編譯為
js
import { ref } from 'vue'
let count = ref(0)
trackChange(count)
如我們所見,$$()
是一個充當轉義提示的宏:在$$()
內的響應式變數不會附加.value
。
在函式作用域內返回
如果直接在返回表示式中使用響應式變數,也可能丟失響應性。
ts
function useMouse() {
let x = $ref(0)
let y = $ref(0)
// listen to mousemove...
// doesn't work!
return {
x,
y
}
}
上述返回語句編譯為
ts
return {
x: x.value,
y: y.value
}
為了保持響應性,我們應該返回實際的refs,而不是在返回時的當前值。
同樣,我們可以使用$$()
來修復這個問題。在這種情況下,可以直接在返回的物件上使用$$()
- $$()
呼叫內的任何響應式變數的引用都將保持對其底層refs的引用
ts
function useMouse() {
let x = $ref(0)
let y = $ref(0)
// listen to mousemove...
// fixed
return $$({
x,
y
})
}
在解構屬性上使用$$()
$$()
對解構屬性也有效,因為它們也是響應式變數。編譯器將使用toRef
進行轉換以提高效率
ts
const { count } = defineProps<{ count: number }>()
passAsRef($$(count))
編譯為
js
setup(props) {
const __props_count = toRef(props, 'count')
passAsRef(__props_count)
}
TypeScript 整合
Vue 為這些宏提供了型別定義(全域性可用),所有型別都將按預期工作。與標準 TypeScript 語義沒有不相容性,因此語法將與所有現有工具配合使用。
這也意味著這些宏可以在任何允許有效 JS / TS 的檔案中工作 - 而不僅限於 Vue SFCs。
由於宏是全域性可用的,它們的型別需要顯式引用(例如,在env.d.ts
檔案中)
ts
/// <reference types="vue/macros-global" />
當從vue/macros
顯式匯入宏時,型別將無需宣告全域性即可正常工作。
顯式選擇
不再在核心中支援
以下僅適用於 Vue 版本 3.3 及以下。支援已在 Vue 核心版本 3.4 及以上以及@vitejs/plugin-vue
版本 5.0 及以上中刪除。如果您打算繼續使用轉換,請遷移到Vue Macros。
Vite
- 需要
@vitejs/plugin-vue@>=2.0.0
- 適用於 SFCs 和 js(x)/ts(x) 檔案。在應用轉換之前對檔案執行快速使用檢查,因此對於不使用宏的檔案不應有效能成本。
- 注意,
reactivityTransform
現在是一個外掛根級選項,而不是作為script.refSugar
巢狀,因為它不僅影響 SFCs。
js
// vite.config.js
export default {
plugins: [
vue({
reactivityTransform: true
})
]
}
vue-cli
- 目前僅影響 SFCs
- 需要
vue-loader@>=17.0.0
js
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
return {
...options,
reactivityTransform: true
}
})
}
}
Plain webpack
+ vue-loader
- 目前僅影響 SFCs
- 需要
vue-loader@>=17.0.0
js
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
reactivityTransform: true
}
}
]
}
}