Трансформация реактивности
Экспериментальная возможность
Преобразование реактивности было экспериментальной функцией и была удалена в последнем выпуске 3.4. Пожалуйста, прочитайте о причинах здесь.
Если вы всё ещё собираетесь использовать эту функцию, она теперь доступна через плагин Vue Macros.
Специфика для Composition API
Преобразование реактивности является специфической функцией Composition-API и требует шага сборки.
Refs и реактивные переменные
С момента появления Composition API одним из основных нерешенных вопросов является использование ссылок по сравнению с реактивными объектами. При деструктуризации реактивных объектов легко потерять реактивность, в то время как при использовании refs может быть неудобно использовать .value
везде. Кроме того, .value
легко пропустить, если не использовать систему типов.
Преобразование реактивности во Vue - это преобразование на этапе компиляции, которое позволяет нам писать код, подобный этому:
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 реактивности, возвращающее refs, будет иметь эквивалент в виде макроса с $
-префиксом. К таким 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)
Деструктуризация с помощью $()
Обычно функция композиции возвращает объект refs, а для получения этих refs используется деструктуризация. Для этого в reactivity transform предусмотрен макрос $()
:
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
уже является ref, то toRef(__temp, 'x')
просто вернет его как есть, и никакого дополнительного ref создано не будет. Если деструктурированное значение не является ref (например, функция), оно все равно будет работать - значение будет обернуто в ref, так что остальная часть кода будет работать как положено.
Деструктуризация $()
работает как с реактивными объектами, так и с обычными объектами, содержащими refs.
Преобразование существующих refs в реактивные переменные с помощью $()
В некоторых случаях мы можем иметь обернутые функции, которые также возвращают refs. Однако компилятор Vue не сможет заранее узнать, что функция будет возвращать ref. В таких случаях макрос $()
также может быть использован для преобразования всех существующих refs в реактивные переменные:
js
function myCreateRef() {
return ref(0)
}
let count = $(myCreateRef())
Деструктуризация реактивных входных параметров
Существуют две болезненные точки с текущим использованием defineProps()
в <script setup>
:
Как и в случае с
.value
, для сохранения реактивности необходимо всегда обращаться к входным параметрам какprops.x
. Это означает, что нельзя деструктурироватьdefineProps
, поскольку полученные в результате деструктуризации переменные не являются реактивными и не будут обновляться.При использовании объявления props только для типа, нет простого способа объявить значения по умолчанию для props. Для этой цели мы ввели API
withDefaults()
, но он по-прежнему неудобен в использовании.
Мы можем решить эти проблемы, применив преобразование во время компиляции, когда defineProps
используется с деструктуризацией, аналогично тому, что мы видели ранее с $()
:
html
<script setup lang="ts">
interface Props {
msg: string
count?: number
foo?: string
}
const {
msg,
// работает значение по умолчанию
count = 1,
// локальные псевдонимы также работают здесь,
// мы используем псевдоним `props.foo` для `bar`
foo: bar
} = defineProps<Props>()
watchEffect(() => {
// будет регистрироваться при каждом изменении входного параметра
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
, возникает проблема "потери реактивности" при передаче реактивных переменных через границы функций. Это может произойти в двух случаях:
Передача в функцию в качестве аргумента
Дана функция, ожидающая в качестве аргумента ref, например:
ts
function trackChange(x: Ref<number>) {
watch(x, (x) => {
console.log('x изменился!')
})
}
let count = $ref(0)
trackChange(count) // не работает!
Приведенный выше случай не будет работать должным образом, поскольку он компилируется в:
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)
// слушаем движение мыши...
// не работает!
return {
x,
y
}
}
Приведенный выше оператор возврата компилируется в:
ts
return {
x: x.value,
y: y.value
}
Для того чтобы сохранить реактивность, мы должны возвращать фактические refs, а не текущее значение в момент возврата.
И снова мы можем использовать $$()
для решения этой проблемы. В этом случае $$()
можно использовать непосредственно на возвращаемом объекте - любые ссылки на реактивные переменные внутри вызова $$()
будут сохранять ссылки на их базовые refs:
ts
function useMouse() {
let x = $ref(0)
let y = $ref(0)
// слушаем движение мыши...
// исправлено
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 SFC.
Поскольку макросы доступны глобально, на их типы необходимо явно ссылаться (например, в файле env.d.ts
):
ts
/// <reference types="vue/macros-global" />
При явном импорте макросов из vue/macros
тип будет работать без объявления глобальных переменных.
Явное включение
Больше не поддерживается во vue/core
Все ниже применимо только до версии 3.3 или ниже. Поддержка была убрана во Vue core 3.4
и выше, также как и в @vitejs/plugin-vue
5.0 и выше. Если вы хотите пользоваться transform
, то лучше мигрируйте на Vue Macros
Vite
- Требуется
@vitejs/plugin-vue@>=2.0.0
- Применяется к SFC и файлам js(x)/ts(x). Перед применением преобразования выполняется быстрая проверка использования файлов, поэтому для файлов, не использующих макросы, не должно быть никаких потерь производительности.
- Обратите внимание, что
reactivityTransform
теперь является опцией корневого уровня плагина, а не вложенной вscript.refSugar
, поскольку она влияет не только на SFC.
js
// vite.config.js
export default {
plugins: [
vue({
reactivityTransform: true
})
]
}
vue-cli
- В настоящее время влияет только на SFC
- Требуется
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
}
})
}
}
Обычный webpack
+ vue-loader
- В настоящее время влияет только на SFC
- Требуется
vue-loader@>=17.0.0
js
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
reactivityTransform: true
}
}
]
}
}