Основы реактивности
Стиль API
На этой и многих других страницах руководства примеры приведены в двух вариантах: Options API и Composition API. Сейчас вы просматриваете версию для Composition 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
Смотрите также: Типизация
ref()
Чтобы получить доступ к ссылкам в шаблоне компонента, объявите и верните их из функции setup()
компонента:
js
import { ref } from 'vue'
export default {
// `setup` — это специальный хук, предназначенный для Composition API.
setup() {
const count = ref(0)
// разворачиваем ref в шаблоне
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 необходим в 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>
, автоматически используются в шаблоне того же компонента. Думайте о шаблоне как о функции JavaScript, объявленной в той же области видимости — она, естественно, имеет доступ ко всему, что объявлено рядом с ней.
Совет
В остальной части руководства мы будем использовать синтаксис 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()
}
}
Ещё одна приятная особенность реактивных ссылок заключается в том, что в отличие от обычных переменных, вы можете передавать ссылки в функции, сохраняя доступ к последнему значению и связи с реактивностью. Это особенно полезно при рефакторинге сложной логики в многократно используемый код.
Более подробно система реактивности рассматривается в разделе Реактивность в деталях.
Глубокая реактивность
Ссылки могут содержать значения любого типа, включая глубоко вложенные объекты, массивы или встроенные в JavaScript структуры данных типа Map
.
ref
сделает его значение глубоко реактивным. Это означает, что вы можете ожидать обнаружения изменений даже при мутации вложенных объектов или массивов:
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()
, о чём речь пойдет ниже.
Также можно отказаться от глубокой реактивности с помощью поверхностных ссылок. Для неглубоких ссылок на реактивность отслеживается только доступ к .value
. Неглубокие ссылки можно использовать для оптимизации производительности, избегая затрат на наблюдение за большими объектами, или в случаях, когда внутреннее состояние управляется внешней библиотекой.
Дальнейшее чтение:
Сроки обновлений DOM
Когда вы изменяете реактивное состояние, DOM обновляется автоматически. Однако следует отметить, что обновления DOM не применяются синхронно. Вместо этого Vue буферизирует их до «следующего тика» в цикле обновления, чтобы каждый компонент обновлялся только один раз, независимо от того, сколько изменений состояния вы произвели.
Чтобы дождаться завершения обновления DOM после изменения состояния, вы можете использовать Global 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>
Реактивные объекты представляют собой объекты Proxy и ведут себя так же, как и обычные объекты. Разница в том, что Vue может перехватывать доступ и мутацию всех свойств реактивного объекта для отслеживания и запуска реактивности.
reactive()
глубоко преобразует объект: Вложенные объекты также оборачиваются reactive()
при обращении к ним. Он также вызывается ref()
, когда значение ссылки является объектом. Как и в случае с поверхностными ссылками, существует API shallowReactive()
для отказа от глубокой реактивности.
Реактивный Proxy против оригинального
Важно отметить, что возвращаемое значение из reactive()
— это Proxy исходного объекта, который не равен исходному объекту:
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()
имеет несколько ограничений:
Ограниченные типы значений: работает только для объектных типов (объекты, массивы и коллекции объектов по ключу, такие как
Map
иSet
). Он не может содержать примитивные типы, такие какстрока
,число
илибулево
.Невозможно заменить весь объект: поскольку отслеживание реактивности Vue работает через доступ к свойствам, мы всегда должны сохранять одну и ту же ссылку на реактивный объект. Это означает, что мы не можем легко «заменить» реактивный объект, поскольку реактивная связь с первой ссылкой теряется:
jslet state = reactive({ count: 0 }) // указанная выше ссылка ({ count: 0 }) больше не отслеживается // (реактивная связь теряется!) state = reactive({ count: 1 })
Несовместимо с деструктуризацией: когда мы деструктурируем свойство примитивного типа реактивного объекта в локальные переменные или когда мы передаем это свойство в функцию, мы теряем реактивную связь:
jsconst 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')])
// need .value here
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// need .value here
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 }}
.