Skip to content

Перетворення реактивності

Застаріла експериментальна функція

Перетворення реактивності було експериментальною функцією, і її було визначено застарілою. Будь ласка, прочитайте про обґрунтування тут.

Згодом його буде видалено з ядра Vue у наступному другорядному випуску.

  • Щоб перейти з нього, скористайтеся цим інструментом командного рядка, який може автоматизувати процес.
  • Якщо ви все ще маєте намір використовувати його, тепер він доступний через плагін Vue Macros.

Специфічний для композиційного API

Перетворення реактивності є специфічною функцією Композиційного API і потребує етапу побудування.

Референції або реактивні змінні?

З моменту появи Композиційного API одним із основних невирішених питань є використання референцій проти реактивних об'єктів. Легко втратити реактивність під час деструктурування реактивних об'єктів, у той час як використання .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 реактивності, який повертає референції, матиме еквівалент макросу з префіксом $. Ці API включають:

Ці макроси доступні глобально та їх не потрібно імпортувати, якщо ввімкнено Перетворення реактивності, але ви можете додатково імпортувати їх із 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>:

  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,
    // локальне псевдонім також працює
    // тут ми створюємо `bar` - псевдонім до `props.foo`
    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
  }
}

Наведений вище вираз повернення компілюється у:

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, а не лише в SFC Vue.

Оскільки макроси доступні глобально, їхні типи мають бути з явними посиланнями (наприклад, у файлі env.d.ts):

ts
/// <reference types="vue/macros-global" />

Під час явного імпорту макросів із vue/macros тип працюватиме без оголошення глобальних змінних.

Явна згода

WARNING

Наступне стосується лише версії 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
        }
      }
    ]
  }
}
Перетворення реактивності has loaded