Перейти к содержанию

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

Стиль API

На этой и многих других страницах руководства примеры приведены в двух вариантах: Options API и Composition API. Сейчас вы просматриваете версию для Options APIComposition 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 имена, начинающиеся с любого из этих символов.

Реактивный Proxy против оригинального

В Vue 3 данные становятся реактивными благодаря использованию объектов Proxy. Пользователям, перешедшим с 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

Смотрите также: Типизация 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()
  }
}

Ещё одна приятная особенность реактивных ссылок заключается в том, что в отличие от обычных переменных, вы можете передавать ссылки в функции, сохраняя доступ к последнему значению и связи с реактивностью. Это особенно полезно при рефакторинге сложной логики в многократно используемый код.

Более подробно система реактивности рассматривается в разделе Реактивность в деталях.

Объявление методов

Чтобы добавить методы к экземпляру компонента, мы используем опцию methods. Это должен быть объект, содержащий нужные методы:

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

Vue автоматически привязывает значение this для methods, чтобы оно всегда ссылалось на экземпляр компонента. Это гарантирует, что метод сохранит правильное значение this, если он используется в качестве слушателя событий или обратного вызова. Вам следует избегать использования стрелочных функций при определении методов, так как это не позволит Vue привязать соответствующее значение this:

js
export default {
  methods: {
    increment: () => {
      // ПЛОХО: Здесь нет доступа к `this`!
    }
  }
}

Как и все остальные свойства экземпляра компонента, методы доступны из шаблона компонента. Внутри шаблона они чаще всего используются в качестве слушателей событий:

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

Попробовать в Песочнице

В приведённом выше примере метод increment будет вызван при нажатии на <кнопку>.

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

В 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.

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 обновлен
}
js
import { nextTick } from 'vue'

export default {
  methods: {
    async increment() {
      this.count++
      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() имеет несколько ограничений:

  1. Ограниченные типы значений: работает только для объектных типов (объекты, массивы и коллекции объектов по ключу, такие как Map и Set). Он не может содержать примитивные типы, такие как строка, число или булево.

  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')])
// 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 }}.

Методы с сохранением состояния

В некоторых случаях нам может понадобиться динамически создавать функцию-метод, например, создавать обработчик событий с отсроченным доступом:

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() {
      // ... реагируем на щелчок ...
    }
  }
}
Основы реактивности