Skip to content

Основи реактивності

Вподобання API

Ця сторінка та багато інших розділів гіду містять різний контент для опційного АРІ і композиційного АРІ. Вашим поточним налаштуванням є Опційний АРІКомпозиційний АРІ. Ви можете перемикатися між стилями API за допомогою перемикача «Вподобання API» у верхній частині лівої бічної панелі.

Оголошення реактивного стану

У Options API ми використовуємо опцію data, щоб оголосити реактивний стан компонента. Значення параметра має бути функцією, яка повертає об’єкт. Vue викличе функцію під час створення нового екземпляра компонента та обгорне повернутий об’єкт у свою систему реактивності. Будь-які властивості верхнього рівня цього об’єкта проксуються на рівні екземпляру компонента (this у методах і хуках життєвого циклу):

js
export default {
  data() {
    return {
      count: 1
    }
  },

  // `mounted` - це хук життєвого циклу, який ми пояснимо пізніше
  mounted() {
    // `this` вказує на екземпляр компонента
    console.log(this.count) // => 1

    // реактивні властивості також можуть бути змінені
    this.count = 2
  }
}

Спробуйте в пісочниці

Ці властивості екземпляра додаються лише під час першого створення екземпляра, тому вам потрібно переконатися, що всі вони присутні в об’єкті, що повертається функцією data. За потреби використовуйте null, undefined або будь-яке інше підмінне значення для властивостей, де значення властивості ще не доступне.

Можна додати нову властивість безпосередньо до this, не включаючи її в data. Однак властивості, додані таким чином, не зможуть ініціювати реактивні оновлення.

Vue використовує префікс $, коли розкриває власні вбудовані API через екземпляр компонента. Він також резервує префікс _ для внутрішніх властивостей. Слід уникати використання імен для властивостей data верхнього рівня, які починаються з будь-якого з цих символів.

Реактивний проксі та вихідне значення

У Vue 3 дані стають реактивними завдяки використанню JavaScript проксі. Користувачі, які знайомі з Vue 2, повинні пам’ятати про таке виключення:

js
export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // false
  }
}

Коли ви використовуєте this.someObject після його ініціації, значення є реактивним проксі оригінального newObject. На відміну від Vue 2, оригінальний newObject залишається недоторканим і не буде зроблений реактивним: переконайтеся, що завжди отримуєте доступ до реактивного стану як властивості this.

Оголошення реактивного стану

ref()

В Composition API рекомендований спосіб оголосити реактивний стан — це використовувати функцію ref():

js
import { ref } from 'vue'

const count = ref(0)

ref() приймає аргумент і повертає його в об’єкті ref із властивістю .value:

js
const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

Також до вашої уваги: Типізація референцій

Щоб використовувати реактивний стан у шаблоні компонента, оголосіть та поверніть їх із функції setup() компонента:

js
import { ref } from 'vue'

export default {
  // `setup` — це спеціальний хук, призначений для композиційного API.
  setup() {
    const count = ref(0)

    // виділення стану до шаблону
    return {
      count
    }
  }
}
template
<div>{{ count }}</div>

Зверніть увагу, що нам не потрібно було додавати .value під час використання посилання в шаблоні. Для зручності посилання автоматично розгортаються під час використання всередині шаблонів (з кількома застереженнями).

Ви також можете змінити посилання безпосередньо в обробниках подій:

template
<button @click="count++">
  {{ count }}
</button>

Для більш складної логіки ми можемо оголосити функції, які змінюють посилання в тій самій області видимості та виставити їх як методи поряд зі станом:

js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // .value is needed in JavaScript
      count.value++
    }

    // не забудьте виділити функцію так само.
    return {
      count,
      increment
    }
  }
}

Виділені методи зазвичай використовуються як слухачі подій:

template
<button @click="increment">
  {{ count }}
</button>

Ось приклад, опублікований на Codepen, без використання інструментів збірки.

<script setup>

Розкриття стану та методів вручну за допомогою setup() може бути заскладним. На щастя, цього можна уникнути, використовуючи Однофайлові компоненти (SFC). Ми можемо спростити використання за допомогою <script setup>:

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

Спробуйте в пісочниці

Імпорт верхнього рівня, змінні та функції, оголошені в <script setup>, автоматично придатні для використання в шаблоні того самого компонента. Подумайте про шаблон як про функцію JavaScript, оголошену в тій самій області видимості – вона природно має доступ до всього, що оголошено поруч із нею.

TIP

У решті посібника ми будемо в основному використовувати синтаксис SFC + <script setup> для прикладів коду Composition API, оскільки це найпоширеніше використання для розробників Vue.

Якщо ви не використовуєте SFC, ви можете використовувати Composition API за допомогою параметра setup().

Чому референції?

Вам може бути цікаво, навіщо нам потрібні посилання з .value замість простих змінних. Щоб пояснити це, нам потрібно буде коротко обговорити, як працює система реактивності Vue.

Коли ви використовуєте посилання в шаблоні та змінюєте значення посилання пізніше, Vue автоматично виявляє зміни та відповідно оновлює DOM. Це стало можливим завдяки системі реактивності на основі відстеження залежностей. Коли компонент рендериться вперше, Vue відстежує кожне посилання, яке було використано під час рендерингу. Пізніше, коли посилання мутується, це ініціює повторний рендеринг для компонентів, які його відстежують.

У стандартному JavaScript немає способу виявити доступ або мутацію простих змінних. Однак ми можемо перехопити операції отримання та встановлення властивостей об’єкта за допомогою методів getter та setter.

Властивість .value дає Vue можливість виявити, коли посилання було доступне або змінене. Під капотом Vue виконує відстеження у своєму геттері та виконує запуск у своєму сеттері. Концептуально ви можете думати про посилання як про об’єкт, який виглядає так:

js
// псевдокод, а не реальна реалізація
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

Ще одна приємна риса посилань полягає в тому, що на відміну від простих змінних, ви можете передавати посилання у функції, зберігаючи доступ до останнього значення та зв’язку реактивності. Це особливо корисно під час рефакторингу складної логіки в багаторазовий код.

Система реактивності обговорюється більш детально в розділі Реактивність поглиблено.

Оголошення методів

Щоб додати методи до екземпляра компонента, ми використовуємо опцію methods. Це має бути об’єкт, що містить потрібні методи:

js
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // методи можна викликати в хуках життєвого циклу або інших методах!
    this.increment()
  }
}

Vue автоматично прив’язує значення this для methods, щоб воно завжди вказувало на екземпляр компонента. Це гарантує, що метод зберігає правильне значення this, якщо він використовується як слухач подій або функція зворотнього виклику. Вам слід уникати використання стрілочних функцій під час визначення methods, оскільки це заважає Vue зв’язати відповідне значення this:

js
export default {
  methods: {
    increment: () => {
      // ПОГАНО: немає доступу до `this`!
    }
  }
}

Подібно до всіх інших властивостей екземпляра компонента, methods доступні у шаблоні компонента. Усередині шаблону вони найчастіше використовуються як слухачі подій:

template
<button @click="increment">{{ count }}</button>

Спробуйте в пісочниці

У наведеному вище прикладі метод increment буде викликаний, коли буде натиснуто <button>.

Глибока реактивність

У Vue стан глибоко реактивний за замовчуванням. Це означає, що ви можете очікувати, що зміни будуть виявлені, навіть коли ви змінюєте вкладені об’єкти або масиви:

js
export default {
  data() {
    return {
      obj: {
        nested: { count: 0 },
        arr: ['foo', 'bar']
      }
    }
  },
  methods: {
    mutateDeeply() {
      // це працюватиме, як очікується
      this.obj.nested.count++
      this.obj.arr.push('baz')
    }
  }
}

Посилання можуть містити будь-які типи значень, у тому числі глибоко вкладені об’єкти, масиви або вбудовані структури даних JavaScript, такі як Map.

Посилання зробить його значення глибоко реактивним. Це означає, що ви можете очікувати, що зміни будуть виявлені, навіть коли ви змінюєте вкладені об’єкти або масиви:

js
import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // це працюватиме, як очікується
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

Непримітивні значення перетворюються на реактивні проксі через reactive(), який обговорюється нижче.

Також можна відмовитися від глибокої реактивності за допомогою shallow refs. Для неглибоких посилань реактивність відстежується доступ лише до .value. Неглибокі посилання можна використовувати для оптимізації продуктивності, уникаючи витрат на спостереження за великими об’єктами, або у випадках, коли внутрішнім станом керує зовнішня бібліотека.

Подальше читання:

Час оновлення DOM

Коли ви змінюєте реактивний стан, DOM оновлюється автоматично. Однак слід зазначити, що оновлення DOM не відбувається синхронно. Натомість Vue буферизує їх до «наступного тіку» в циклі оновлення, щоб гарантувати, що кожен компонент буде оновлено лише один раз, незалежно від того, скільки змін стану ви зробили.

Щоб дочекатися завершення оновлення DOM після зміни стану, ви можете скористатися nextTick() з глобального API:

js
import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // DOM оновлено
}
js
import { nextTick } from 'vue'

export default {
  methods: {
    async increment() {
      this.count++
      await nextTick()
      // DOM оновлено
    }
  }
}

reactive()

Існує інший спосіб оголосити реактивний стан за допомогою API reactive(). На відміну від референції, яка загортає внутрішнє значення в спеціальний об’єкт, reactive() робить сам об’єкт реактивним:

js
import { reactive } from 'vue'

const state = reactive({ count: 0 })

Дивіться також: Типізація реактивності

Використання в шаблоні:

template
<button @click="state.count++">
  {{ state.count }}
</button>

Реактивні об'єкти є проксі-серверами JavaScript і поводяться так само, як звичайні об'єкти. Різниця полягає в тому, що Vue здатний перехоплювати доступ і мутацію всіх властивостей реактивного об'єкта для відстеження реактивності та запуску.

reactive() глибоко перетворює об'єкт: вкладені об'єкти також загортаються за допомогою reactive() під час доступу. Він також викликається ref() внутрішньо, коли значення ref є об'єктом. Подібно до неглибоких посилань, існує також shallowReactive() API для відмови від глибокої реактивності.

Реактивний проксі проти оригінального

Важливо зауважити, що значення, яке повертає reactive(), є проксі оригінального об'єкту, який не є вихідним об'єктом:

js
const raw = {}
const proxy = reactive(raw)

// проксі не є вихідним об'єктом.
console.log(proxy === raw) // false

Реактивним є лише проксі – зміна вихідного об’єкта не призведе до оновлення. Тому найкраща практика під час роботи з реактивною системою Vue — використовувати виключно проксі-версії вашого стану.

Щоб забезпечити послідовний доступ до проксі, виклик reactive() для того самого об’єкта завжди повертає той самий проксі, а виклик reactive() для існуючого проксі також повертає той самий проксі:

js
// виклик reactive() для того самого об’єкта повертає той самий проксі
console.log(reactive(raw) === proxy) // true

// виклик reactive() для проксі повертає сам себе
console.log(reactive(proxy) === proxy) // true

Це правило також стосується вкладених об’єктів. Через глибоку реактивність вкладені об’єкти всередині реактивного об’єкта також є проксі:

js
const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

Обмеження reactive()

reactive() API має два обмеження:

  1. Він працює лише для типів об’єктів (об’єктів, масивів і типів колекцій, таких як Map і Set). Він не може містити примітивні типи, такі як string, number або boolean.

  2. Оскільки відстеження реактивності у Vue працює через доступ до властивостей, ми повинні завжди зберігати те саме посилання на реактивний об’єкт. Це означає, що ми не можемо просто «замінити» реактивний об’єкт, оскільки реактивний звязок також буде втрачено:

    js
    let state = reactive({ count: 0 })
    
    // наведене вище посилання ({ count: 0 })
    // більше не відстежується (реактивне з’єднання втрачено!)
    state = reactive({ count: 1 })
  3. Не підтримує деструктурування: коли ми деструктуруємо властивість примітивного типу реактивного об'єкта на локальні змінні або коли ми передаємо цю властивість у функцію, ми втратимо зв'язок реактивності:

js
const state = reactive({ count: 0 })

// count від'єднується від state.count під час деструктурування.
   let { count } = state
   // не впливає на початковий стан
   count++

   // функція отримує звичайне число і
   // не зможе відстежувати зміни в state.count
   // ми повинні передати весь об'єкт, щоб зберегти реактивність
   callSomeFunction(state.count)

Через ці обмеження ми рекомендуємо використовувати ref() як основний API для оголошення реактивного стану.

Додаткові відомості про розгортання референцій

Як властивість реактивного об'єкта

Референція автоматично розгортається під час доступу або змінюється як властивість реактивного об'єкта. Іншими словами, вона поводиться як звичайна властивість:

js
const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

Якщо новий об'єкт-референція призначається властивості існуючого обʼєкта-референції, то, він замінить старий обʼєкт-рефренцію:

js
const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// вихідний обʼєкт-референцію тепер відʼєднано від state.count
console.log(count.value) // 1

Розгортання об'єктів-референцій відбувається лише тоді, коли вони вкладені в глибоко реактивний об'єкт. Це не відбувається, коли до них звертаються як до властивості неглибоко реактивного об'єкта.

Застереження щодо масивів і колекцій

На відміну від реактивних об'єктів, розгортання не виконується, коли до референції звертаються як до елемента реактивного масиву або рідного типу колекції, наприклад Map:

js
const books = reactive([ref('Vue 3 Guide')])
// потрібно використовувати .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// потрібно використовувати .value
console.log(map.get('count').value)

Застереження під час розгортання в шаблонах

Розгортання посилання в шаблонах застосовується, лише якщо посилання є властивістю верхнього рівня в контексті відтворення шаблону.

У наведеному нижче прикладі count і object є властивостями верхнього рівня, а object.id - ні:

js
const count = ref(0)
const object = { id: ref(1) }

Отже, цей вираз працює, як очікувалося:

template
{{ count + 1 }}

...а цей НІ:

template
{{ object.id + 1 }}

Відображеним результатом буде [object Object]1, тому що object.id не розгортається під час обчислення виразу та залишається об'єктом посилання. Щоб виправити це, ми можемо деструктурувати id у властивість верхнього рівня:

js
const { id } = object
template
{{ id + 1 }}

Тепер результат візуалізації буде 2.

Інша річ, на яку слід звернути увагу, полягає в тому, що посилання розгортається, якщо воно є остаточним оціненим значенням текстової інтерполяції (тобто тегом {{ }}), тому наступне відобразить 1:

template
{{ object.id }}

Це лише зручна функція інтерполяції тексту та еквівалентна {{ object.id.value }}.

Методи з використанням стану

У деяких випадках нам може знадобитися динамічно створювати метод, наприклад, створюючи відкладений обробник подій:

js
import { debounce } from 'lodash-es'

export default {
  methods: {
    // Відкладений за допомогою Lodash
    click: debounce(function () {
      // ... реакція на клік ...
    }, 500)
  }
}

Однак цей підхід є проблематичним для компонентів, які повторно використовуються, тому що відкладена функція має стан: вона підтримує деякий внутрішній стан протягом часу. Якщо кілька екземплярів компонентів використовують одну й ту саму відкладену функцію, вони заважатимуть один одному.

Щоб підтримувати незалежність відкладених функцій кожного екземпляра компонента, ми можемо створити відкладену функцію в хуку created життєвого циклу компонента:

js
export default {
  created() {
    // кожен екземпляр тепер має свою власну копію відкладеного обробника
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // також гарна ідея скасувати таймер
    // коли компонент буде демонтовано
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... реакція на клік ...
    }
  }
}
Основи реактивності has loaded