Перейти к содержанию

Reactivity Transform

Утратившая силу экспериментальная функция

Reactivity Transform была экспериментальной функцией и уже устарела. Пожалуйста, ознакомьтесь с обоснованием.

В конечном итоге она будет удалена из ядра Vue в одном из будущих минорных выпусков.

  • Чтобы отказаться от неё, ознакомьтесь с этим инструментом командной строки, который может автоматизировать этот процесс.
  • Если вы всё ещё собираетесь использовать эту функцию, то теперь она доступна через плагин Vue Macros.

Composition-API-specific

Reactivity Transform является специфической функцией Composition API и требует этапа сборки.

Ссылки против реактивных переменных

С момента появления 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++
}

Каждый Reactivity API, возвращающий рефссылки, будет иметь эквивалент макроса с префиксом $. Эти API включают в себя:

Эти макросы доступны глобально и не нуждаются в импорте при включении Reactivity Transform, но вы можете импортировать их из vue/macros, если хотите быть более явными:

js
import { $ref } from 'vue/macros'

let count = $ref(0)

Деструктуризация с $()

Обычно функция композиции возвращает объект ref-ссылок и использует деструктуризацию для извлечения этих ref-ссылок. Для этого в 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 уже является ссылкой, то toRef(__temp, 'x') просто вернет её как есть, и никакой дополнительной ссылки создано не будет. Если деструктурированное значение не является ссылкой (например, функция), он всё равно будет работать — значение будет обёрнуто в ссылку, так что остальная часть кода будет работать, как и ожидалось.

Деструктуризация $() работает как с реактивными объектами, так и с обычными объектами, содержащими ссылки.

Преобразование существующих ссылок в реактивные переменные с помощью $()

В некоторых случаях мы можем иметь обёрнутые функции, которые также возвращают ссылки. Однако компилятор Vue не сможет заранее узнать, что функция будет возвращать ссылку. В таких случаях макрос $() также может быть использован для преобразования всех существующих ссылок в реактивные переменные:

js
function myCreateRef() {
  return ref(0)
}

let count = $(myCreateRef())

Деструктуризация реактивных параметров

Есть две болевые точки при текущем использовании defineProps() в <script setup>:

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

  2. При использовании объявления параметра только для типа не существует простого способа объявить значения по умолчанию для параметра. Именно для этой цели мы ввели 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, это создает проблему «потери реактивности», когда мы передаем реактивные переменные через границы функции. Это может произойти в двух случаях:

Передача в функцию в качестве аргумента

Учитывая функцию, которая ожидает ссылку в качестве аргумента, например:

ts
function trackChange(x: Ref<number>) {
  watch(x, (x) => {
    console.log('x changed!')
  })
}

let count = $ref(0)
trackChange(count) // не работает!

Приведённый выше случай не будет работать должным образом, поскольку он компилируется так:

ts
let count = ref(0)
trackChange(count.value)

Здесь count.value передается как число, тогда как trackChange ожидает фактическую ссылку. Это можно исправить, обернув 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)

  // слушаем mousemove...

  // не работает!
  return {
    x,
    y
  }
}

Приведённый выше оператор return компилируется в:

ts
return {
  x: x.value,
  y: y.value
}

Чтобы сохранить реактивность, мы должны возвращать фактические ссылки, а не текущее значение во время возврата.

Опять же, мы можем использовать $$(), чтобы исправить это. В этом случае $$() можно использовать непосредственно в возвращаемом объекте — любая ссылка на реактивные переменные внутри вызова $$() сохранит ссылку на их базовые ссылки:

ts
function useMouse() {
  let x = $ref(0)
  let y = $ref(0)

  // слушаем mousemove...

  // исправлено
  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 3.3 и ниже. Поддержка ядра будет удалена в версии 3.4 и выше. Если вы собираетесь продолжать использовать трансформацию, перейдите на 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
        }
      }
    ]
  }
}
Reactivity Transform