Skip to content

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

Выбор API

Эта страница и многие другие главы в этом руководстве, содержат различный контент для Options API и Composition API. В настоящее время выбран Options APIComposition API. Можно переключаться между двумя API с помощью переключателя "Выбрать API" в верхней части левой боковой панели.

Объявление реактивного состояния

В Options API используется опция data для объявления реактивного состояния компонента. Значение опции должно быть функцией, которая возвращает объект. Vue будет вызывать функцию при создании нового экземпляра компонента и обернет возвращаемый объект в свою систему реактивности. Любые свойства верхнего уровня этого объекта проксируются на экземпляр компонента (this в методах и хуках жизненного цикла):

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

  // `mounted` это хук жизненного цикла Vue, который объясним позже
  mounted() {
    // `this` ссылается на экземпляр компонента
    console.log(this.count) // => 1

    // данные также могут быть мутированы
    this.count = 2
  }
}

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

Эти свойства экземпляра добавляются только при первом создании экземпляра, поэтому необходимо убедиться, что все они присутствуют в объекте, возвращаемом функцией data. При необходимости используйте null, undefined или другое значение для свойств, где нужное значение еще недоступно.

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

Vue использует префикс $, когда предоставляет свои собственные встроенные API в экземпляре компонента. Vue также оставляет префикс _ для внутренних свойств. Следует избегать использования имён для свойств верхнего уровня 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` это специальный хук, предназначенный для С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()
  }
}

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

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

Бесплатный урок по методам Vue.js

Для добавления методов к экземпляру компонента, используется опция 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` компонента не будет!
    }
  }
}

Как и все остальные свойства экземпляра компонента, методы из объекта 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. Shallow refs можно использовать для оптимизации производительности, избегая затрат на наблюдение за большими объектами, или в случаях, когда внутреннее состояние управляется внешней библиотекой.

Дополнительное чтение:

Время обновления DOM

Когда вы изменяете реактивное состояние, DOM обновляется автоматически. Однако следует отметить, что обновления DOM не применяются синхронно. Вместо этого Vue буферизирует их до "следующего тика" в цикле обновления, чтобы гарантировать, что каждый компонент обновляется только один раз, независимо от того, сколько изменений состояния вы сделали.

Чтобы дождаться завершения обновления DOM после изменения состояния, вы можете использовать глобальный 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>

Реактивные объекты представляют собой 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() имеет два ограничения:

  1. Ограниченные типы значений: работает только для типов объектов (objects, arrays, и типы коллекций такие как 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
    // не влияет на 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 }}.

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

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

js
import { debounce } from 'lodash-es'

export default {
  methods: {
    // Дебаунсинг с помощью Lodash
    click: debounce(function () {
      // ... реагировать на нажатие ...
    }, 500)
  }
}

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

Чтобы исправить проблему, описанную выше, можно использовать функцию debounce в хуке жизненного цикла created:

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