Глобальные свойства 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 APIwatch() / 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 предоставляют мощные инструменты для создания гибких и переиспользуемых компонентов.

Ключевые моменты

  1. Разделяйте шаблон и скрипт. Все $-свойства доступны в <template> напрямую, но в <script setup> нужны соответствующие импорты: useSlots(), useAttrs(), nextTick(), watch().
  2. Composition API — приоритет. Вместо this.$watchwatch() / watchEffect(). Вместо this.$refsuseTemplateRef() (3.5+) или ref(). Вместо this.$nextTicknextTick().
  3. Reactive Props Destructure (Vue 3.5) — главное нововведение Vue 3.5. Деструктуризация пропсов с сохранением реактивности, значения по умолчанию без withDefaults.
  4. Избегайте $parent и $root. Они нарушают инкапсуляцию. Используйте provide/inject, emit или Pinia.
  5. $props + $attrs — мощная пара. $props передаёт объявленные пропсы, $attrs — всё остальное. Вместе они идеальны для компонентов-обёрток.
  6. Проверяйте наличие слотов. Используйте v-if="$slots.footer" в шаблоне или useSlots() в скрипте, чтобы не рендерить пустые обёртки.
Написать комментарий