Глобальные свойства Vue 3: $props, $attrs, $emit, $refs

Содержание
Глобальные свойства с префиксом $ доступны в любом компоненте без импорта. В <template> ими пользуются напрямую ($props, $attrs, $emit…), а в <script setup> у каждого есть аналог-функция из 'vue'. Ниже — что это, когда применять и чем заменять в Composition API.
Основные глобальные объекты
$props
Содержит: Все пропсы компонента
Назначение: Упрощенная передача всех пропсов дочерним компонентам
⚠️ Примечание:
$propsсуществует с Vue 2 и доступен в Vue 3 с самого начала — это не нововведение Vue 3.5. Нововведение Vue 3.5 — Reactive Props Destructure (см. следующий раздел).
<script setup lang="ts">
interface Props {
label?: string
disabled?: boolean
size?: 'small' | 'medium' | 'large'
variant?: 'primary' | 'secondary'
}
defineProps<Props>()
</script>
<template>
<!-- Передаем все пропсы дочернему компоненту -->
<BaseButton v-bind="$props">
<slot />
</BaseButton>
<!-- Можно комбинировать с дополнительными пропсами -->
<BaseButton
v-bind="$props"
:loading="isLoading"
@click="handleClick"
>
Submit
</BaseButton>
</template>
Преимущества:
- ✅ Автоматическая передача новых пропсов
- ✅ Type safety с TypeScript
- ✅ Упрощение компонентов-оберток
- ✅ Меньше boilerplate кода
Reactive Props Destructure ⭐ (Новое в Vue 3.5)
Назначение: Деструктуризация пропсов в <script setup> с сохранением реактивности
До Vue 3.5 деструктуризация пропсов приводила к потере реактивности. Начиная с Vue 3.5 можно деструктурировать прямо из defineProps — переменные остаются реактивными.
<script setup lang="ts">
// ✅ Vue 3.5+: деструктуризация с сохранением реактивности
const {
label = 'Default',
disabled = false,
size = 'medium'
} = defineProps<{
label?: string
disabled?: boolean
size?: 'small' | 'medium' | 'large'
}>()
// label, disabled, size — реактивны!
</script>
<template>
<button :class="[`btn-${size}`]" :disabled="disabled">
{{ label }}
</button>
</template>
Использование с watch и computed:
<script setup lang="ts">
import { computed, watch } from 'vue'
const { count = 0, multiplier = 1 } = defineProps<{
count?: number
multiplier?: number
}>()
// ✅ count и multiplier реактивны
const total = computed(() => count * multiplier)
watch(() => count, (newVal) => {
console.log('count изменился:', newVal)
})
</script>
<template>
<p>count: {{ count }}, total: {{ total }}</p>
</template>
⚠️ Важно — граница реактивности: деструктурированные пропсы реактивны в
<template>,computedиwatchEffect. Но при прямой передаче в обычную функцию,watchили composable значение «замораживается» и перестаёт быть реактивным. Передавать нужно геттер:
watch(() => count, (v) => {}) // ✅ реактивно
watch(count, (v) => {}) // ❌ ошибка компиляции
useComposable(() => count) // ✅ тоже геттер (внутри: toValue(count))
Значения по умолчанию (без withDefaults):
<script setup lang="ts">
const {
title = 'Без названия',
variant = 'primary',
items = [] // ✅ напрямую, не фабрика
} = defineProps<{
title?: string
variant?: 'primary' | 'secondary'
items?: string[]
}>()
</script>
<template>
<div :class="`card--${variant}`">
<h2>{{ title }}</h2>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
📌 Значения по умолчанию для объектов и массивов: при деструктуризации записываются напрямую (
items = [],config = {}), а не фабричной функцией. Оборачивание в() => []нужно только дляwithDefaults; при деструктуризации компилятор сам гарантирует каждому экземпляру отдельную копию. Записьitems = () => []сделала бы значением по умолчанию саму функцию — иv-forпо такому пропу сломался бы.
Преимущества:
- ✅ Реактивность сохраняется при деструктуризации (в
<template>,computed,watchEffect) - ✅ Значения по умолчанию прямо в синтаксисе JS (без
withDefaults) - ✅ Чистый и лаконичный код
- ✅ Полная поддержка TypeScript
$attrs
Содержит: Все атрибуты, не являющиеся пропсами
Включает: class, id, style, data-*, aria-*, обработчики событий
⚠️ Отличие от Vue 2: в Vue 3
$attrsвключаетclass,styleи события (в Vue 2 они не входили, а события были в$listeners, который удалён в Vue 3).
📌 В шаблоне: $attrs доступен напрямую. В <script setup>: используйте useAttrs() из 'vue'.
<script setup> // Пропсы определены явно defineProps(['title', 'disabled']) // $attrs будет содержать все остальное </script>
<template>
<div>
<h2>{{ title }}</h2>
<!-- Передаем все "неопознанные" атрибуты input'у -->
<input
v-bind="$attrs"
:disabled="disabled"
placeholder="Введите текст..."
/>
</div>
</template>
Использование:
<!-- Родительский компонент --> <MyInput title="Имя пользователя" disabled="false" class="custom-input" data-testid="user-name" @focus="handleFocus" maxlength="50" />
$attrs получит:
{
class: "custom-input",
"data-testid": "user-name",
onFocus: handleFocus,
maxlength: "50"
}
Контроль наследования атрибутов:
<script setup>
// Отключаем автоматическое наследование атрибутов
defineOptions({
inheritAttrs: false
})
defineProps(['title'])
</script>
<template>
<div class="wrapper">
<!-- Явно указываем, куда передать атрибуты -->
<input v-bind="$attrs" />
<span class="helper-text">{{ title }}</span>
</div>
</template>
$slots
Содержит: Все слоты компонента
Позволяет: Динамически работать со слотами
📌 В шаблоне: $slots доступен напрямую. В <script setup>: используйте useSlots().
<script setup>
import { computed, useSlots } from 'vue'
const slots = useSlots()
// Проверяем наличие слотов
const hasHeader = computed(() => !!slots.header)
const hasFooter = computed(() => !!slots.footer)
const slotNames = computed(() => Object.keys(slots))
</script>
<template>
<div class="card">
<!-- Условный рендеринг слота -->
<header v-if="hasHeader" class="card-header">
<slot name="header" />
</header>
<!-- Основной контент -->
<main class="card-body">
<slot />
</main>
<!-- Динамический слот с параметрами -->
<slot
name="content"
:user="currentUser"
:actions="availableActions"
/>
<!-- Слот с fallback контентом -->
<footer v-if="hasFooter" class="card-footer">
<slot name="footer">
<p>Футер по умолчанию</p>
</slot>
</footer>
<!-- Отладочная информация -->
<div v-if="isDev" class="debug">
Доступные слоты: {{ slotNames.join(', ') }}
</div>
</div>
</template>
Продвинутая работа со слотами (⚠️ нестандартные приёмы):
⚠️ Пометка: примеры ниже рендерят слот-функции напрямую через
<component :is="...">— рабочий, но не каноничный и недокументированный приём: опирается на то, что слот — это функция, возвращающая VNode. Штатные способы — тег<slot name="...">или render-функция (h()). Применять осознанно: есть зависимость от внутренней реализации.
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
// Можем передавать слоты как компоненты
const renderSlot = (slotName, props = {}) => {
return slots[slotName]?.(props)
}
</script>
<template>
<div>
<!-- ⚠️ Нестандартно: рендер результата слот-функции через :is -->
<component :is="() => renderSlot('custom', { data: 'test' })" />
<!-- ⚠️ Нестандартно: передача самой слот-функции в :is -->
<div v-for="(slot, name) in $slots" :key="name">
<h3>Слот: {{ name }}</h3>
<component :is="slot" />
</div>
</div>
</template>
$emit
Назначение: Эмиттинг событий из компонента
📌 В шаблоне: $emit(...) доступен напрямую. В <script setup>: используйте const emit = defineEmits<{...}>().
<script setup lang="ts">
// Определяем события
const emit = defineEmits<{
update: [value: string]
change: [event: Event]
submit: [data: FormData]
error: [message: string]
'custom-event': [payload: { id: number; status: string }]
}>()
// Можем также использовать в скрипте
const handleSave = () => {
emit('submit', formData)
}
</script>
<template>
<div>
<!-- Простой эмит -->
<button @click="$emit('update', 'new-value')">
Update
</button>
<!-- Эмит с объектом -->
<button
@click="$emit('custom-event', { id: 1, status: 'completed' })"
>
Complete Task
</button>
<!-- Эмит с event объектом -->
<input @change="$emit('change', $event)" />
<!-- Условный эмит -->
<button
@click="
isValid
? $emit('submit', formData)
: $emit('error', 'Invalid data')
"
>
Submit
</button>
</div>
</template>
$refs
Содержит: Ссылки на элементы и компоненты с ref
📌 В шаблоне: $refs доступен напрямую. В <script setup>: $refs недоступен — используйте useTemplateRef() (Vue 3.5+) или ref() (до Vue 3.5).
Способ 1 — useTemplateRef() ⭐ (Vue 3.5+, рекомендуемый):
Имя переменной не обязано совпадать с ref="...". Автоматический вывод типов TypeScript.
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// ✅ Vue 3.5+: аргумент должен совпадать с ref="..." в шаблоне
const inputEl = useTemplateRef<HTMLInputElement>('my-input')
const childRef = useTemplateRef<InstanceType<typeof ChildComponent>>('child')
onMounted(() => {
inputEl.value?.focus()
childRef.value?.someMethod()
})
</script>
<template>
<div>
<input ref="my-input" v-model="inputValue" />
<ChildComponent ref="child" :data="componentData" />
</div>
</template>
Способ 2 — ref() (до Vue 3.5):
Имя переменной обязано совпадать с ref="..." в шаблоне.
<script setup>
import { ref, onMounted } from 'vue'
// Имя переменной = имя ref в шаблоне
const myInput = ref(null)
onMounted(() => {
myInput.value?.focus()
})
</script>
<template> <input ref="myInput" /> </template>
Динамические ref'ы в v-for:
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// Vue 3.5+ — автоматически собирает массив
const itemRefs = useTemplateRef('items')
onMounted(() => {
console.log(itemRefs.value) // [li, li, li, ...] — порядок НЕ гарантирован
})
</script>
<template>
<ul>
<li v-for="item in list" :key="item.id" ref="items">
{{ item.name }}
</li>
</ul>
</template>
⚠️ Порядок не гарантирован: массив, который
useTemplateRefсобирает вv-for, не обязан совпадать по порядку с исходным списком (это поведение зафиксировано в документации Vue). Полагаться на индексы нельзя — сопоставлять элементы поdata-атрибуту или находить нужный поиском по массиву.
$parent
Содержит: Ссылку на родительский компонент
⚠️ Осторожно: Нарушает инкапсуляцию, используйте умеренно
📌 В шаблоне: $parent доступен напрямую. В <script setup>: $parent недоступен — нужен getCurrentInstance() (не рекомендуется, лучше использовать provide/inject или emit).
<script setup>
import { getCurrentInstance, onMounted } from 'vue'
const instance = getCurrentInstance()
// Получаем данные от родителя (не рекомендуется!)
onMounted(() => {
const parent = instance?.parent?.proxy
if (parent) {
console.log('Родительский компонент:', parent)
}
})
</script>
<template>
<div>
<!-- Доступ к данным родителя -->
<p v-if="$parent">
Значение от родителя: {{ $parent.parentValue }}
</p>
<!-- Вызов методов родителя -->
<button @click="$parent.handleFromChild('hello')">
Вызвать метод родителя
</button>
</div>
</template>
$root
Содержит: Ссылку на корневой компонент приложения
📌 В шаблоне: $root доступен напрямую. В <script setup>: $root недоступен — используйте getCurrentInstance() или лучше provide/inject для глобальных данных.
<script setup>
import { getCurrentInstance, onMounted } from 'vue'
const instance = getCurrentInstance()
// Доступ к корню (лучше provide/inject)
onMounted(() => {
const root = instance?.root?.proxy
console.log('Корневое приложение:', root)
})
</script>
<template>
<div>
<!-- Доступ к глобальным данным -->
<div v-if="$root.user">
Пользователь: {{ $root.user.name }}
</div>
<!-- Глобальные настройки -->
<div :class="{ dark: $root.isDarkMode }">
Контент с учетом темы
</div>
<!-- Вызов глобальных методов -->
<button @click="$root.showGlobalNotification('Привет!')">
Показать уведомление
</button>
</div>
</template>
Глобальные методы
Помимо объектов-данных, Vue предоставляет и глобальные методы: в шаблоне — $nextTick и $watch, в <script setup> — импорт nextTick, watch / watchEffect из 'vue'.
$nextTick / nextTick
Назначение: Выполнить код после следующего обновления DOM
📌 В шаблоне: доступен как $nextTick. В <script setup>: используйте импортированный nextTick из 'vue'.
<script setup>
import { ref, nextTick } from 'vue'
const message = ref('Привет')
const inputRef = ref()
const updateMessage = async () => {
message.value = 'Обновленное сообщение'
// ✅ В <script setup> — используем импортированный nextTick
nextTick(() => {
console.log('DOM обновлен!')
inputRef.value?.focus()
})
// Или с async/await
await nextTick()
console.log('DOM точно обновлен!')
}
// Пример с анимацией
const animateElement = async () => {
isVisible.value = true
// Ждем обновления DOM
await nextTick()
// Теперь элемент в DOM, можем анимировать
const element = document.querySelector('.animated')
element?.animate([
{ opacity: 0, transform: 'translateY(-20px)' },
{ opacity: 1, transform: 'translateY(0)' }
], { duration: 300 })
}
</script>
<template>
<div>
<p>{{ message }}</p>
<input ref="inputRef" />
<button @click="updateMessage">Обновить</button>
</div>
</template>
watch / watchEffect (замена this.$watch)
Назначение: Отслеживание изменений реактивных данных
⚠️ Важно:
$watch— это метод из Options API (this.$watch). В<script setup>он не существует — используйтеwatchиwatchEffectиз'vue'.
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const userData = ref({ name: '', email: '' })
// Простой watch
const stopWatching = watch(searchQuery, (newVal, oldVal) => {
console.log(`Поиск изменился: ${oldVal} -> ${newVal}`)
performSearch(newVal)
})
// watch с опциями
watch(userData, (newUserData) => {
localStorage.setItem('userData', JSON.stringify(newUserData))
}, {
deep: true, // Глубокое наблюдение
immediate: true, // Выполнить немедленно
flush: 'post' // После обновления DOM
})
// Множественный watch (наблюдение за несколькими источниками)
watch(
[() => userData.value.name, () => userData.value.email],
([newName, newEmail], [oldName, oldEmail]) => {
console.log('Имя или email изменились')
updateProfile({ name: newName, email: newEmail })
}
)
// watchEffect — сам отслеживает все реактивные зависимости
watchEffect(() => {
console.log('searchQuery:', searchQuery.value)
// Будет вызван при любом изменении searchQuery
})
</script>
<template>
<div>
<input
v-model="searchQuery"
placeholder="Поиск..."
/>
<!-- Можем останавливать watcher -->
<button @click="stopWatching">
Остановить отслеживание поиска
</button>
</div>
</template>
📌 Справка: доступность в шаблоне vs <script setup>
Сводка раздела: что доступно в <template> напрямую и чем это заменить в <script setup>.
| Свойство | В <template> | В <script setup> |
|---|---|---|
$props | ✅ Напрямую | defineProps() |
$attrs | ✅ Напрямую | useAttrs() |
$slots | ✅ Напрямую | useSlots() |
$emit | ✅ Напрямую | defineEmits() |
$refs | ✅ Напрямую | useTemplateRef() (3.5+) или ref() |
$parent | ✅ Напрямую | getCurrentInstance() (не рек.) |
$root | ✅ Напрямую | getCurrentInstance() (не рек.) |
$nextTick | ✅ Напрямую | nextTick() из 'vue' |
$watch | ✅ Options API | watch() / watchEffect() из 'vue' |
$el | ✅ Напрямую | template ref на корневой узел (getCurrentInstance() — только для отладки) |
Общее правило: все $-свойства доступны в <template> напрямую. В <script setup> нужно использовать соответствующие импорты и функции из 'vue'.
Устаревшие но доступные свойства
Эти свойства Vue 3 унаследовал от Options API. Формально они доступны, но в новом коде у каждого есть рекомендованная замена.
$el (Composition API)
Содержит: Корневой DOM элемент компонента
⚠️ Внимание: Прямого
$elв<script setup>нет. Доступ возможен черезgetCurrentInstance().proxy.$el, но это путь для отладки и крайних случаев. Рекомендуемый способ получить корневой DOM-элемент — template ref на корневой узел (useTemplateRef()в 3.5+ илиref()).
<script setup>
import { getCurrentInstance, onMounted } from 'vue'
const instance = getCurrentInstance()
onMounted(() => {
// Доступ к корневому элементу
const rootElement = instance?.proxy?.$el
console.log('Корневой элемент:', rootElement)
// Манипуляции с DOM
rootElement?.classList.add('mounted')
})
</script>
<template>
<div class="my-component">
<!-- Этот div будет доступен как $el -->
Контент компонента
</div>
</template>
$data (Options API наследие)
Содержит: Реактивные данные компонента
В Composition API: Не используется, данные создаются через ref/reactive
<script>
// Пример только для понимания, не используйте в новом коде
export default {
data() {
return {
counter: 0,
user: { name: 'John' }
}
},
mounted() {
console.log('Все данные:', this.$data)
// { counter: 0, user: { name: 'John' } }
}
}
</script>
<template>
<div>
<!-- В шаблоне $data содержит все data() свойства -->
<pre>{{ JSON.stringify($data, null, 2) }}</pre>
</div>
</template>
$options (Options API)
Содержит: Опции компонента
<script>
export default {
name: 'MyComponent',
customOption: 'custom value',
created() {
console.log('Имя компонента:', this.$options.name)
console.log('Кастомная опция:', this.$options.customOption)
console.log('Все опции:', this.$options)
}
}
</script>
Практические примеры
Соберём изученное в реальные компоненты — здесь $props, $attrs, слоты и события работают вместе.
Компонент-обертка с полной передачей свойств
<script setup lang="ts">
// CustomInput.vue - обертка над обычным input
interface Props {
label?: string
required?: boolean
helperText?: string
}
defineProps<Props>()
// defineModel регистрирует prop modelValue и update:modelValue
const model = defineModel<string>()
defineOptions({ inheritAttrs: false })
</script>
<template>
<div class="input-wrapper">
<label v-if="label" class="input-label">
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<!-- Передаем все $attrs (class, placeholder, maxlength и т.д.) -->
<input
v-model="model"
v-bind="$attrs"
class="input-field"
/>
<p v-if="helperText" class="helper-text">
{{ helperText }}
</p>
</div>
</template>
<style scoped>
.input-wrapper { /* стили */ }
.input-field { /* стили */ }
.required { color: red; }
</style>
Динамический компонент с умным роутингом
<script setup>
import { computed } from 'vue'
const route = useRoute()
const router = useRouter()
// Умное определение активного пункта меню
const activeSection = computed(() => {
return route.path.split('/')[1] || 'home' // первый сегмент пути
})
const navigation = [
{ name: 'home', label: 'Главная', icon: 'home' },
{ name: 'profile', label: 'Профиль', icon: 'user' },
{ name: 'settings', label: 'Настройки', icon: 'settings' }
]
const navigateTo = (section) => {
router.push(`/${section}`)
}
</script>
<template>
<nav class="navigation">
<button
v-for="item in navigation"
:key="item.name"
:class="{ active: activeSection === item.name }"
@click="navigateTo(item.name)"
>
<i :class="`icon-${item.icon}`" />
{{ item.label }}
</button>
</nav>
<!-- Отладочная информация -->
<div class="debug" v-if="$root.debugMode">
<p>Активная секция: {{ activeSection }}</p>
<p>Текущий роут: {{ $route.path }}</p>
</div>
</template>
Продвинутый компонент с условными слотами
<script setup lang="ts">
import { computed, useSlots } from 'vue'
interface Props {
title?: string
variant?: 'success' | 'error' | 'warning' | 'info'
dismissible?: boolean
icon?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'info',
dismissible: false,
icon: true
})
const emit = defineEmits<{
dismiss: []
}>()
// Проверяем наличие слотов
const slots = useSlots()
const hasActions = computed(() => !!slots.actions)
const hasIcon = computed(() => !!slots.icon)
const iconName = computed(() => {
const icons = {
success: 'check-circle',
error: 'x-circle',
warning: 'alert-triangle',
info: 'info'
}
return icons[props.variant]
})
</script>
<template>
<div
class="alert"
:class="`alert--${variant}`"
role="alert"
v-bind="$attrs"
>
<!-- Иконка -->
<div v-if="icon" class="alert__icon">
<slot name="icon">
<i :class="`icon-${iconName}`" />
</slot>
</div>
<!-- Контент -->
<div class="alert__content">
<h4 v-if="title" class="alert__title">{{ title }}</h4>
<div class="alert__message">
<slot />
</div>
</div>
<!-- Действия -->
<div v-if="hasActions" class="alert__actions">
<slot name="actions" />
</div>
<!-- Кнопка закрытия -->
<button
v-if="dismissible"
class="alert__dismiss"
@click="$emit('dismiss')"
aria-label="Закрыть"
>
<i class="icon-x" />
</button>
</div>
</template>
Best Practices
✅ Рекомендуется
1. Используйте $props для компонентов-оберток:
<!-- ✅ Хорошо -->
<BaseButton v-bind="$props" @click="handleClick">
{{ label }}
</BaseButton>
2. Проверяйте наличие слотов:
<!-- ✅ Хорошо --> <footer v-if="$slots.footer" class="card-footer"> <slot name="footer" /> </footer>
3. Используйте nextTick() для DOM манипуляций (в шаблоне — $nextTick, в скрипте — nextTick из 'vue'):
// ✅ Хорошо
const scrollToBottom = async () => {
messages.value.push(newMessage)
await nextTick() // импортированный из 'vue'
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
}
❌ Избегайте
1. Не злоупотребляйте $parent:
<!-- ❌ Плохо --> <button @click="$parent.$parent.grandParentMethod()"> Плохая идея </button>
2. Не используйте $refs для передачи данных:
// ❌ Плохо
const sendDataToChild = () => {
$refs.childComponent.data = newData // Используйте пропсы!
}
3. Не смешивайте Composition API с $data:
<script setup> // ❌ Плохо в Composition API // В Composition API нет $data, используйте ref/reactive </script>
🔧 TypeScript интеграция
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Modal from './Modal.vue'
// Типизированные ссылки (Vue 3.5+)
const inputRef = useTemplateRef<HTMLInputElement>('input')
const modalRef = useTemplateRef<InstanceType<typeof Modal>>('modal')
// Типизированные события
const emit = defineEmits<{
change: [value: string]
submit: [data: FormData]
}>()
// Типизированные пропсы
interface Props {
items: Array<{ id: number; name: string }>
loading?: boolean
}
defineProps<Props>()
</script>
Заключение
Глобальные $-свойства Vue 3 предоставляют мощные инструменты для создания гибких и переиспользуемых компонентов.
Ключевые моменты
- Разделяйте шаблон и скрипт. Все
$-свойства доступны в<template>напрямую, но в<script setup>нужны соответствующие импорты:useSlots(),useAttrs(),nextTick(),watch(). - Composition API — приоритет. Вместо
this.$watch→watch()/watchEffect(). Вместоthis.$refs→useTemplateRef()(3.5+) илиref(). Вместоthis.$nextTick→nextTick(). - Reactive Props Destructure (Vue 3.5) — главное нововведение Vue 3.5. Деструктуризация пропсов с сохранением реактивности, значения по умолчанию без
withDefaults. - Избегайте
$parentи$root. Они нарушают инкапсуляцию. Используйтеprovide/inject,emitили Pinia. - $props + $attrs — мощная пара.
$propsпередаёт объявленные пропсы,$attrs— всё остальное. Вместе они идеальны для компонентов-обёрток. - Проверяйте наличие слотов. Используйте
v-if="$slots.footer"в шаблоне илиuseSlots()в скрипте, чтобы не рендерить пустые обёртки.