ref, reactive, computed, watch, watchEffect, lifecycle hooks и синтаксический сахар script setup
Composition API — это альтернативный способ организации логики компонента, введённый в Vue 3. Вместо разделения по «опциям» (data, methods, computed) логика группируется по смыслу в виде функций.
Представьте компонент управления пользователями: поиск, сортировка, пагинация, модальные окна, загрузка данных. В Options API всё это перемешивается: логика поиска размазана между data, computed и methods. Чтобы понять, как работает одна фича, нужно прыгать между разными секциями файла.
Options API отлично работает для простых компонентов. Но когда компонент превышает 200-300 строк, его становится трудно читать и ещё труднее переиспользовать логику между компонентами. Исторически единственным решением были миксины — но они создавали конфликты имён и скрытые зависимости.
Composition API решает это: вы группируете связанную логику в функции, называемые composables, и переиспользуете их между компонентами без этих проблем.
Функция setup() — это точка входа для Composition API. Она вызывается до создания компонента, до created хука. В ней объявляются все реактивные данные, вычисляемые значения, методы — и всё это возвращается для использования в шаблоне:
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
// Всё что вернём — доступно в шаблоне
const count = ref(0)
function increment() {
count.value++
}
// Обязательно нужно вернуть
return { count, increment }
}
}
</script>Обратите внимание: в setup() нет доступа к this. Это намеренное решение — композируемые функции должны быть чистыми и не зависеть от контекста компонента.
ref() создаёт реактивную ссылку на значение. Оборачивает значение в объект с полем .value. Это необходимо для отслеживания изменений примитивов (строк, чисел, булевых значений):
<script setup>
import { ref } from 'vue'
const count = ref(0) // число
const name = ref('Иван') // строка
const isLoading = ref(false) // булево
const items = ref([]) // массив тоже можно через ref
console.log(count.value) // 0 — доступ через .value в JS
count.value++ // изменение через .value
// В шаблоне Vue автоматически разворачивает .value:
// {{ count }} — не нужно писать {{ count.value }}
</script>
<template>
<p>{{ count }}</p> <!-- Vue сам подставит count.value -->
</template>ref работает с любым типом данных, включая объекты и массивы. Для объектов под капотом используется reactive, поэтому вложенные свойства тоже реактивны.
reactive() создаёт глубоко реактивный объект. Не нужен .value — доступ к свойствам прямой:
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Иван',
email: 'ivan@example.com',
address: {
city: 'Москва',
street: 'Арбат'
}
})
// Прямой доступ — без .value
user.name = 'Пётр'
user.address.city = 'Питер' // Глубокая реактивность работает!
// В шаблоне:
// {{ user.name }}, {{ user.address.city }}
</script>Ограничение reactive: нельзя заменить объект целиком — потеряется реактивность. Правильный способ — обновлять свойства:
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0, user: null })
// НЕПРАВИЛЬНО — потеря реактивности:
// state = reactive({ count: 10, user: { name: 'Иван' } })
// ПРАВИЛЬНО — обновляем свойства:
state.count = 10
state.user = { name: 'Иван' }
</script>Оба инструмента создают реактивные данные, но у каждого свои сценарии применения.
Используйте ref для примитивов (числа, строки, булевы значения) и когда нужно переприсваивать значение целиком:
<script setup>
import { ref, reactive } from 'vue'
// ref — примитивы и значения, которые меняются целиком
const count = ref(0)
const selectedUser = ref(null)
const tags = ref(['vue', 'js'])
// Переприсвоение работает корректно:
selectedUser.value = { id: 1, name: 'Иван' } // OK
tags.value = ['vue', 'ts'] // OK
// reactive — объекты с несколькими полями, стейт формы
const form = reactive({
username: '',
password: '',
rememberMe: false
})
// Удобнее обновлять несколько полей:
Object.assign(form, { username: 'ivan', rememberMe: true })
</script>На практике большинство Vue-разработчиков предпочитают ref везде — его поведение предсказуемее. reactive удобен для форм и сложных стейт-объектов.
computed() создаёт вычисляемое реактивное значение, которое кэшируется и пересчитывается только при изменении зависимостей:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Иван')
const lastName = ref('Петров')
const items = ref([
{ name: 'Яблоко', category: 'fruits', price: 50 },
{ name: 'Банан', category: 'fruits', price: 30 },
{ name: 'Молоко', category: 'dairy', price: 80 }
])
const filter = ref('fruits')
// Простое вычисление
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Сложная фильтрация — пересчитывается только при изменении items или filter
const filteredItems = computed(() =>
items.value.filter(item => item.category === filter.value)
)
// Computed с сеттером
const fullNameWithSetter = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last || ''
}
})
</script>Computed-значение доступно через .value в скрипте и напрямую в шаблоне. Оно только для чтения по умолчанию — для изменения нужен сеттер.
watch() позволяет реагировать на изменения конкретного реактивного значения. Полезен для side effects: запросы к API, логирование, синхронизация:
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
const userId = ref(null)
const user = ref(null)
// Простое наблюдение за ref
watch(searchQuery, (newValue, oldValue) => {
console.log(`Поиск изменился с "${oldValue}" на "${newValue}"`)
performSearch(newValue)
})
// immediate: true — запустить немедленно при инициализации
watch(userId, async (newId) => {
if (newId) {
user.value = await fetchUser(newId)
}
}, { immediate: true })
// deep: true — глубокое наблюдение за объектом
const userProfile = ref({ name: 'Иван', settings: { theme: 'dark' } })
watch(userProfile, (newValue) => {
saveToLocalStorage(newValue)
}, { deep: true })
// Наблюдение за несколькими источниками
watch([searchQuery, userId], ([newQuery, newId]) => {
console.log('Изменилось что-то из этих:', newQuery, newId)
})
// Наблюдение за геттером (вычисляемым выражением)
watch(
() => userProfile.value.settings.theme,
(newTheme) => {
document.body.dataset.theme = newTheme
}
)
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
async function performSearch(query) {
// ...
}
function saveToLocalStorage(data) {
localStorage.setItem('profile', JSON.stringify(data))
}
</script>watchEffect() запускает функцию немедленно и автоматически перезапускает её при изменении любого реактивного значения, использованного внутри. Не нужно явно указывать зависимости:
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Иван')
// Автоматически отслеживает все зависимости — count и name
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
// Этот код запустится сразу и потом при каждом изменении count или name
})
// Очистка ресурсов внутри watchEffect
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('тик', count.value)
}, 1000)
// onCleanup вызывается перед следующим запуском или при размонтировании
onCleanup(() => clearInterval(timer))
})
</script>Разница между watch и watchEffect:
watch — явно указываете что наблюдать, получаете старое и новое значение, ленивый (не запускается сразу по умолчанию)watchEffect — автоматически отслеживает зависимости, запускается сразу, нет доступа к старому значениюВ Composition API хуки жизненного цикла импортируются из Vue и вызываются внутри setup():
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
const data = ref(null)
let pollInterval = null
onMounted(async () => {
// DOM доступен — подходит для работы с DOM, загрузки данных
data.value = await fetchInitialData()
pollInterval = setInterval(pollData, 5000)
})
onUpdated(() => {
// Вызывается после обновления DOM
// Осторожно: не изменяйте данные здесь — это вызовет бесконечный цикл
})
onUnmounted(() => {
// Очищаем ресурсы чтобы избежать утечек памяти
clearInterval(pollInterval)
})
async function fetchInitialData() {
const res = await fetch('/api/data')
return res.json()
}
async function pollData() {
data.value = await fetchInitialData()
}
</script>Важный момент: в Composition API нет хуков beforeCreate и created — функция setup() сама выполняется на этом этапе.
<script setup> — самый удобный способ использовать Composition API. Он устраняет необходимость явно возвращать значения из setup(). Всё что объявлено в <script setup> автоматически доступно в шаблоне:
<template>
<!-- count, doubled, increment — всё доступно напрямую -->
<div>
<p>{{ count }} (удвоено: {{ doubled }})</p>
<button @click="increment">+1</button>
<UserCard :user="currentUser" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import UserCard from './UserCard.vue' // Компоненты тоже не нужно регистрировать!
// Это всё автоматически доступно в шаблоне:
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
const currentUser = ref({ name: 'Иван', role: 'admin' })
</script>Также в <script setup> доступны специальные макросы компилятора: defineProps, defineEmits, defineExpose. Они не нужно импортировать:
<script setup>
// Объявление props
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
// Объявление событий
const emit = defineEmits(['update', 'close'])
function handleUpdate() {
emit('update', { newCount: props.count + 1 })
}
// Что экспортировать для родительского компонента через ref
defineExpose({ handleUpdate })
</script>Главная суперсила Composition API — composables: функции с префиксом use, которые инкапсулируют реактивную логику и могут использоваться в любом компоненте:
<!-- useCounter.js — переиспользуемый composable -->Создадим несколько composables как отдельные файлы:
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = initialValue }
return { count, doubled, increment, decrement, reset }
}// composables/useFetch.js
import { ref, watch } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
async function fetchData() {
isLoading.value = true
error.value = null
try {
const response = await fetch(url.value ?? url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
}
// Если url реактивный — перезапрашиваем при изменении
if (typeof url !== 'string') {
watch(url, fetchData, { immediate: true })
} else {
fetchData()
}
return { data, error, isLoading, refetch: fetchData }
}// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const value = ref(stored ? JSON.parse(stored) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}Теперь используем composables в компонентах:
<template>
<div>
<p>Счётчик: {{ count }} (удвоено: {{ doubled }})</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Сбросить</button>
<div v-if="isLoading">Загрузка...</div>
<div v-else-if="error">Ошибка: {{ error }}</div>
<ul v-else>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter'
import { useFetch } from '@/composables/useFetch'
// Используем два composable одновременно — никаких конфликтов имён
const { count, doubled, increment, decrement, reset } = useCounter(10)
const { data, error, isLoading } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
</script>Composables — это мощный паттерн. В следующих темах мы увидим их в действии при работе с Vue Router, Pinia и Nuxt.
Composition API с <script setup> — современный и предпочтительный способ написания Vue-компонентов. ref и reactive создают реактивные данные, computed кэширует вычисления, watch и watchEffect реагируют на изменения, хуки жизненного цикла позволяют встраиваться в нужный момент. А composables объединяют всё это в переиспользуемые функции — краеугольный камень масштабируемых Vue-приложений.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.