Основы реактивности
Выбор API
Эта страница и многие другие главы в этом руководстве, содержат различный контент для Options API и Composition API. В настоящее время выбран Composition API. Можно переключаться между двумя API с помощью переключателя "Выбрать API" в верхней части левой боковой панели.
Объявление реактивного состояния
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` это специальный хук, предназначенный для Сomposition API.
setup() {
const count = ref(0)
// передайте состояние шаблону
return {
count
}
}
}
template
<div>{{ count }}</div>
Обратите внимание, что нам не нужно добавлять .value
при использовании ссылки в шаблоне. Для удобства ref автоматически "разворачиваются" при использовании внутри шаблонов (с некоторыми предостережениями).
Вы также можете мутировать ref непосредственно в обработчиках событий:
template
<button @click="count++">
{{ count }}
</button>
Для более сложной логики мы можем объявить функции, которые изменяют ref в той же области видимости, и вернуть их как методы вместе с состоянием:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// .value необходимо в JavaScript
count.value++
}
// не забудьте также передать функцию.
return {
count,
increment
}
}
}
Раскрытые методы можно использовать в качестве обработчиков событий:
template
<button @click="increment">
{{ count }}
</button>
Вот живой пример на Codepen, который можно посмотреть без использования каких-либо инструментов сборки.
<script setup>
Ручное предоставление состояния и методов через setup() может быть громоздким. К счастью, этого можно избежать при использовании однофайловых компонентов (SFCs). Мы можем упростить использование с помощью <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>
, автоматически можно использовать в шаблоне того же компонента.
TIP
В остальной части руководства мы будем использовать синтаксис SFC + <script setup>
для примеров кода Composition API, так как это подходит большинству Vue-разработчиков.
Если вы не используете SFC, вы всё равно можете использовать Composition API с помощью опции setup()
.
Почему Refs?
Возможно, вы задаетесь вопросом, почему нам нужны ссылки с .value
, а не обычные переменные. Чтобы объяснить это, нам нужно вкратце рассказать о том, как работает система реактивности Vue.
Когда вы используете ссылку в шаблоне, а затем изменяете ее значение, Vue автоматически обнаруживает это изменение и соответствующим образом обновляет DOM. Это возможно благодаря системе реактивности, основанной на отслеживании зависимостей. Когда компонент рендерится в первый раз, Vue отслеживает каждую ссылку, которая была использована во время рендеринга. В дальнейшем, когда ссылка будет изменена, это запустит повторный рендеринг для компонентов, которые отслеживают ее.
В стандартном JavaScript нет способа обнаружить доступ к обычным переменным или их изменение. Однако мы можем перехватывать операции получения и установки свойств объекта с помощью методов getter и setter.
Свойство .value
дает Vue возможность обнаружить, когда к ссылке обращались или она была изменена. Под капотом Vue выполняет отслеживание в геттере, а срабатывание - в сеттере. Концептуально, вы можете представить себе ref как объект, который выглядит следующим образом:
js
// псевдокод, а не реальная реализация
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
Еще одна приятная особенность рефов заключается в том, что в отличие от обычных переменных, вы можете передавать рефы в функции, сохраняя доступ к последнему значению и связи с реактивностью. Это особенно полезно при рефакторинге сложной логики в многократно используемый код.
Более подробно система реактивности рассматривается в разделе Подробнее о реактивности.
Глубокая реактивность
Рефы могут содержать значения любого типа, включая глубоко вложенные объекты, массивы или встроенные в 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
. Shallow refs можно использовать для оптимизации производительности, избегая затрат на наблюдение за большими объектами, или в случаях, когда внутреннее состояние управляется внешней библиотекой.
Дополнительное чтение:
Время обновления DOM
Когда вы изменяете реактивное состояние, DOM обновляется автоматически. Однако следует отметить, что обновления DOM не применяются синхронно. Вместо этого Vue буферизирует их до "следующего тика" в цикле обновления, чтобы гарантировать, что каждый компонент обновляется только один раз, независимо от того, сколько изменений состояния вы сделали.
Чтобы дождаться завершения обновления DOM после изменения состояния, вы можете использовать глобальный API nextTick():
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// Теперь DOM обновлен
}
reactive()
Есть и другой способ объявить реактивное состояние - с помощью API reactive()
. В отличие от ref, который оборачивает внутреннее значение в специальный объект, reactive()
делает сам объект реактивным:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
См. также: Руководство — Типизация
reactive()
Использование в шаблоне:
template
<button @click="state.count++">
{{ state.count }}
</button>
Реактивные объекты представляют собой JavaScript прокси и ведут себя так же, как обычные объекты. Разница в том, что Vue может перехватывать доступ и мутацию всех свойств реактивного объекта для отслеживания и запуска реактивности.
reactive()
преобразует объект в глубину: вложенные объекты также оборачиваются reactive()
при обращении к ним. Она также вызывается ref()
, когда значение ссылки является объектом. Аналогично неглубоким ссылкам, существует также API shallowReactive()
для отказа от глубокой реактивности.
Реактивный прокси против оригинального
Важно отметить, что возвращаемое значение от 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()
API reactive()
имеет несколько ограничений:
Ограниченные типы значений: работает только для типов объектов (objects, arrays, и типы коллекций такие как
Map
иSet
). Он не может удерживать примитивные типы такие какstring
,number
илиboolean
.Невозможность замены всего объекта: поскольку отслеживание реактивности во Vue работает через доступ к свойствам, мы должны всегда сохранять одну и ту же ссылку на реактивный объект. Это означает, что мы не можем легко "заменить" реактивный объект, поскольку связь с реактивностью первой ссылки теряется:
jslet state = reactive({ count: 0 }) // вышеуказанная ссылка ({ count: 0 }) больше не отслеживается // (реактивность потеряна!) state = reactive({ count: 1 })
Не дружелюбен к деструктуризации: когда мы деструктурируем свойство примитивного типа реактивного объекта в локальные переменные или передаем это свойство в функцию, мы теряем связь с реактивностью:
jsconst state = reactive({ count: 0 }) // При деструктуризации count отсоединяется от state.count. let { count } = state // не влияет на state.count count++ // функция получает простое число и // не сможет отслеживать изменения в state.count // мы должны передать весь объект целиком, чтобы сохранить реактивность callSomeFunction(state.count)
В связи с этими ограничениями мы рекомендуем использовать ref()
в качестве основного API для объявления реактивного состояния.
Дополнительные детали разворачивания Ref
Как свойство реактивного объекта
Ссылка автоматически разворачивается, когда к ней обращаются или она изменяется как свойство реактивного объекта. Другими словами, он ведет себя как обычное свойство:
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
Если новая ref-ссылка назначается свойству, связанному с существующей ref-ссылкой, она заменяет старую ref-ссылку:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// старая ref-ссылка теперь не влияет на state.count
console.log(count.value) // 1
Разворачивание ref-ссылки происходит только при вложении внутри глубокого реактивного объекта. Он не применяется, когда к нему обращаются как к свойству неглубокого реактивного объекта.
Предостережение при работе с массивами и коллекциями
В отличие от реактивных объектов, не происходит разворачивания, когда ref-ссылка доступна как элемент реактивного массива или нативной коллекции, например 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)
Предостережение при разворачивании в шаблонах
Разворачивание ссылок в шаблонах применяется только в том случае, если ref является свойством верхнего уровня в контексте рендеринга шаблона.
В примере ниже 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
не разворачивается при вычислении выражения и остаётся объектом ref. Чтобы исправить это, мы можем деструктурировать id
в свойство верхнего уровня:
js
const { id } = object
template
{{ id + 1 }}
Теперь результатом рендеринга будет 2
.
Следует также отметить, что ссылка разворачивается, если она является конечным значением текстовой интерполяции (т.е. тега {{ }}
), поэтому в следующем случае будет выведено 1
:
template
{{ object.id }}
Это просто удобная функция интерполяции текста, которая эквивалентна {{ object.id.value }}
.