[Декларация Reactive State]
В Composition API рекомендуемый метод для декларации реактивного состояния - использовать функцию ref():
import { ref } from 'vue'
const count = ref(0)
Функция ref() принимает аргумент и возвращает объект-обертку над ним. Доступ к аргументу в этом объекте осуществляется через его свойство .value:
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Чтобы обращаться к ref-фам в template компонента, декларируйте и возвращайте их в функции setup() компонента:
import { ref } from 'vue'
export default {
// `setup` это специальный хук, выделенный для Composition API.
setup() {
const count = ref(0)
// Публикация ref-объекта для шаблона template:
return {
count
}
}
}
После этого можно обращаться к значению ref-объекта в шаблоне компонента следующим образом:
// Шаблон компонента:
< template>
..
< div>{{ count }}< /div>
..
< /template>
Обратите внимание, что в шаблоне не нужно добавлять .value при использовании ref. Для удобства ref-ы автоматически раскрываются, когда используются внутри шаблонов (с некоторыми оговорками).
Вы также можете напрямую мутировать (изменять) ref в обработчиках события:
< button @click="count++">
{{ count }}
< /button>
Для более сложной логики мы можем декларировать функции, которые мутируют ref-ы в той же самой области видимости, и публиковать их как методы рядом с состоянием (значением):
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// .value is needed in JavaScript
count.value++
}
// не забудьте также опубликовать функцию
return {
count,
increment
}
}
}
Опубликованные методы затем можно использовать в обработчиках события:
< button @click="increment">
{{ count }}
< /button>
[< script setup>]
Публикация вручную состояния (state) и методов через setup() может быть утомительной и слишком громоздкой. К счастью, этого можно избежать при использовании однофайловых компонентов (Single-File Components, SFC). Так что мы можем упростить использование функционала реактивности с помощью < script setup>:
< script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
< /script>
< template>
< button @click="increment">
{{ count }}
< /button>
< /template>
Импортирования высокого уровня, переменные и функции, декларируемые в < script setup>, становятся автоматически доступными в шаблоне template того же компонента. Думайте о template как о функции JavaScript, декларированной в той же области видимости - она натурально имеет доступ ко всему, что объявлено рядом.
[Откуда появился ref]
У вас может появиться вопрос, зачем нам нужны эти ref-ы вместе с их .value вместо простых переменных. Чтобы понять это, нужно кратко обсудить, как работает система реактивности Vue.
Когда вы используете ref в шаблоне template, и меняете позже его значение, Vue автоматически детектирует это изменение и обновляет DOM соответствующим образом. Это стало возможным благодаря системе отслеживания зависимостей (dependency-tracking), основанной на системе реактивности. При первом рендеринге компонента Vue отслеживает каждый ref, который был использован во время рендеринга. Позже, когда ref мутирован, система реактивности вызовет триггер для повторного рендеринга для компонентов, которые отслеживают этот ref.
В стандартном JavaScript нет возможности обнаружить доступ или мутацию обычных переменных. Однако мы можем перехватить операции чтения и установки (get и set) объекта, используя методы getter и setter.
Свойство .value дает возможность для Vue определить, был ли доступ к ref на чтение (get) или на мутирование (set). Под капотом Vue выполняет отслеживание в своем getter, и выполняет trigger в своем setter. Концептуально это можно представить себе следующим образом:
// Псевдокод для демонстрации принципа,
// это не реальная реализация:
const myRef = {
_value: 0, get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
Другой приятный бонус ref-фов заключается в том, что в отличие от обычных переменных вы можете передать ref-ы в функции, сохраняя при этом доступ к последнему значению и соединению реактивности. Это особенно полезно при рефакторинге сложный логики в повторно используемом коде.
Более подробно система реактивности обсуждается в [5].
[Глубокая реактивность]
Ref-ы могут содержать значение любого типа, включая глубоко вложенные объекты, массивы или встроенные структуры данных JavaScript наподобие Map.
Объект ref делает свое значение глубоко реактивным. Это означает, что можно ожидать обнаружение изменений даже при мутации вложенных объектов или массивов:
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// это будет работать ожидаемо
obj.value.nested.count++
obj.value.arr.push('baz')
}
Не примитивные значения превращаются в реактивные proxy через reactive(), что будет обсуждаться далее.
Можно также отказаться от глубокой реактивности с помощью shallow ref. Для таких ref-ов отслеживается реактивность доступа только для .value. Shallow ref может использоваться для оптимизации производительности, избегая затрат на наблюдение над большими объектами, или в тех случаях, когда внутреннее состояние управляется внешней библиотекой.
См. также:
Reduce Reactivity Overhead for Large Immutable Structures
Integration with External State Systems
[Тайминг обновления DOM]
Когда вы мутируете реактивное состояние, DOM обновляется автоматически. Однако следует отметить, что обновления DOM не применяются синхронно. Вместо этого Vue буферизирует обновления до "следующего тика" в цикле обновления (update cycle) для гарантии, что каждый компонент обновится только один раз независимо от того, сколько изменений состояния было сделано.
Чтобы дождаться завершения обновления DOM после изменения состояния, вы можете использовать глобальную API-функцию nextTick():
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// Теперь DOM обновился
}
[reactive()]
Другой способ объявить реактивное состояние - использовать API-функцию reactive(). В отличие от ref, который оборачивает внутреннее значение в специальных объект, функция reactive() делает реактивным сам объект:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
Использование в template:
< button @click="state.count++">
{{ state.count }}
< /button>
Реактивные объекты Vue это JavaScript-прокси [6], и они ведут себя как обычные объекты. Разница в том, что Vue способен перехватывать доступ и мутацию всех свойств реактивного объекта для отслеживания реактивности и запуска trigger.
Функция reactive() глубоко преобразует объект: вложенные объекты также оборачиваются при доступе к ним через reactive(). Это также приводит к внутреннему вызову ref(), когда значение ref это объект. Подобно shallow ref также есть API-функция shallowReactive(), которая отменяет глубокую реактивность объекта.
Reactive Proxy vs. Original. Важно отметить, что возвращаемое значение из reactive() является Proxy [6] исходного объекта, который не равен исходному объекту:
const raw = {}
const proxy = reactive(raw)
// proxy НЕ эквивалентен своему оригиналу.
console.log(proxy === raw) // false
При этом реактивен только proxy, так что мутирование оригинального объекта не приведет к срабатыванию обновлений. Поэтому лучшая практика для работы с системой реактивности Vue: эксклюзивное использование proxi-версий вашего состояния.
Чтобы обеспечить согласованный доступ к proxy, вызов reactive() на том же объекте всегда возвратит тот же proxy, а вызов reactive() на существующем proxy также возвратит тот же proxy:
// Вызов reactive() на том же самом объекте возвратит
// тот же самый proxy:
console.log(reactive(raw) === proxy) // true
// Вызов reactive() на proxy возвратит его самого:
console.log(reactive(proxy) === proxy) // true
Это правило применяется также и к вложенным объектам. Из-за глубокой реактивности вложенные объекты внутри reactive-объекта также являются proxy:
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
Ограничения reactive(). В применении reactive() действуют несколько ограничений:
1. Ограниченные типы значений: это работает только с типами объектов (object, array и collection-типы, такие как Map и Set). Не получается хранить примитивные типы, такие как string, число или boolean.
2. Нельзя заменить весь объект целиком: поскольку отслеживание Vue reactivity работает только над доступом к свойствам, мы всегда должны сохранять одну и ту же ссылку на reactive-объект. Это значит, что мы не можем легко "заменить" reactive-объект, потому что связь реактивности с первой ссылкой теряется:
let state = reactive({ count: 0 })
// Верхняя ссылка ({ count: 0 }) больше не отслеживается
// (соединение реактивности потеряно!)
state = reactive({ count: 1 })
3. Not destructure-friendly, т. е. отсутствие поддержки деструкции: когда мы деструктурируем reactive-объекты со свойством примитивного типа в локальные переменные, или когда передаем такое свойство в функцию, мы потеряем reactivity-соединение:
const state = reactive({ count: 0 })
// count отключен от state.count при деструктурировании.
let { count } = state
// Не влияет на оригинальное состояние:
count++
// В этом примере функция принимает простое число,
// и не сможет отследить изменения state.count.
// Нам следует вместо этого передать объект целиком,
// чтобы сохранить реактивность.
callSomeFunction(state.count)
Из-за наличия этих ограничений рекомендуется использовать ref() как primary API для декларации reactive state.
[Дополнительные подробности разворачивания ref]
Reactive Object в качестве свойства. Сам ref автоматически разворачивается (unwrapped), когда к нему осуществляется доступ на чтение или запись как к свойству reactive-объекта. Другими словами, он ведет себя как обычное свойство:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
Если новый ref присвоен свойству, связанному с существующим ref, то это заменит старый ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// Оригинальный ref теперь отключен от state.count
console.log(count.value) // 1
Ref unwrapping происходит только когда есть вложение внутри deep reactive объекта. Это не применимо, когда происходит доступ к ref как к свойству shallow reactive объекта.
Оговорки для массивов и коллекций. В отличие от reactive объектов, не будет происходить unwrapping, когда к ref происходит обращение как к элементу reactive-массива или типу native-коллекции наподобие Map:
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)
Оговорки при unwrapping в template. Ref unwrapping в шаблонах template применимо только если ref это свойство верхнего уровня в контексте рендера template.
В следующем примере count и object это свойства верхнего уровня, но object.id таковым не является:
const count = ref(0)
const object = { id: ref(1) }
Таким образом, это выражение в шаблоне работает ожидаемо:
... однако это выражение НЕ РАБОТАЕТ:
Результат рендера будет [object Object]1, потому что object.id не был unwrapped при вычислении выражения, и остался объектом ref. Чтобы это исправить, мы можем деструктурировать id в свойство верхнего уровня:
В шаблоне:
Теперь результат рендера будет 2.
Еще следует отменить, что ref не становится unwrapped, если он конечное вычисляемое значение текстовой интерполяции (например тег {{ }}), так что следующее приведет к рендеру 1:
Это просто фича удобства для интерполяции текста, и эквивалентно {{ object.id.value }}.