Skip to content

Трансформация реактивности

Экспериментальная возможность

Преобразование реактивности было экспериментальной функцией и была удалена в последнем выпуске 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 относятся:

Эти макросы доступны глобально и не требуют импорта при включении 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>:

  1. Как и в случае с .value, для сохранения реактивности необходимо всегда обращаться к входным параметрам как props.x. Это означает, что нельзя деструктурировать defineProps, поскольку полученные в результате деструктуризации переменные не являются реактивными и не будут обновляться.

  2. При использовании объявления 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.
vite.config.js
js
export default {  plugins: [  vue({  reactivityTransform: true  })  ] }

vue-cli

  • В настоящее время влияет только на SFC
  • Требуется vue-loader@>=17.0.0
vue.config.js
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
webpack.config.js
js
module.exports = {  module: {  rules: [  {  test: /\.vue$/,  loader: 'vue-loader',  options: {  reactivityTransform: true  }  }  ]  } }
Трансформация реактивностиУже загружено