响应式转换
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
}
}
]
}
}