Vue 3 Script Setup та defineModel у 2026: Сучасний Синтаксис та Питання на Співбесідах

Вичерпний посібник з синтаксису script setup у Vue 3 та макросу defineModel. Огляд reactive props destructure, типізації TypeScript та питань на співбесідах.

Vue 3 Script Setup та defineModel посібник

Синтаксис script setup у Vue 3 трансформує підхід до написання компонентів, усуваючи шаблонний код та значно покращуючи інтеграцію з TypeScript. У поєднанні з defineModel, введеним експериментально у Vue 3.3 та стабілізованим у Vue 3.4, створення компонентів з двостороннім зв'язуванням даних потребує значно менше коду порівняно з еквівалентом в Options API.

Деструктуризація Props у Vue 3.5

Починаючи з Vue 3.5, властивості (props) можна деструктуризувати безпосередньо з defineProps, зберігаючи реактивність. У більшості випадків це усуває необхідність використання toRefs.

Основи Script Setup у Vue 3.5

Синтаксис <script setup> — це синтаксичний цукор часу компіляції для Composition API всередині однофайлових компонентів (Single File Components). Змінні, оголошені на верхньому рівні, автоматично доступні в шаблоні, а імпорти можна використовувати напряму без реєстрації.

vue
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'

// Props з TypeScript інтерфейсом
const props = defineProps<{
  user: User
  showEmail?: boolean
}>()

// Реактивний стан
const isExpanded = ref(false)

// Computed властивість
const displayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

// Емітер подій з типізованим payload
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// Методи — це звичайні функції
function handleSelect() {
  emit('select', props.user.id)
}
</script>

<template>
  <div class="user-card" @click="handleSelect">
    <h3>{{ displayName }}</h3>
    <p v-if="showEmail">{{ user.email }}</p>
    <button @click.stop="isExpanded = !isExpanded">
      {{ isExpanded ? 'Згорнути' : 'Розгорнути' }}
    </button>
  </div>
</template>

Компілятор автоматично обробляє інструкції експорту та реєстрацію компонентів. Це зменшує когнітивне навантаження порівняно з явним поверненням значень з функції setup().

Деструктуризація Реактивних Props (Vue 3.5+)

До Vue 3.5 деструктуризація props порушувала реактивність, оскільки деструктуризація в JavaScript створює знімок значення на момент виконання. Vue 3.5 впроваджує відстеження на рівні компілятора, яке зберігає реактивність для деструктуризованих props.

vue
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'

// Деструктуризація з значеннями за замовчуванням — залишається реактивною в 3.5+
const { query = '', placeholder = 'Пошук...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch працює на деструктуризованих props
watch(
  () => query,
  (newQuery) => {
    console.log('Запит змінився:', newQuery)
  }
)
</script>

<template>
  <input
    :value="query"
    :placeholder="placeholder"
    @input="$emit('update:query', ($event.target as HTMLInputElement).value)"
  />
</template>

Цей синтаксис потребує Vue 3.5 або новішої версії. Документація Vue 3.5 release announcement описує повну поведінку деструктуризації реактивних props.

defineModel для Двостороннього Зв'язування

До defineModel реалізація v-model на власному компоненті вимагала оголошення prop та ручного випромінювання події оновлення. Цей патерн зустрічається в безлічі Vue кодових баз:

vue
<!-- Старий спосіб — багатослівний, але явний -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

function updateValue(event: Event) {
  emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>

З defineModel той самий компонент стає значно коротшим:

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel створює ref, синхронізований з v-model
const model = defineModel<string>({ default: '' })
</script>

<template>
  <input
    v-model="model"
    type="text"
    class="text-input"
  />
</template>

Батьківський компонент використовує стандартний синтаксис v-model:

vue
<!-- ParentForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import TextInput from './TextInput.vue'

const username = ref('')
</script>

<template>
  <TextInput v-model="username" />
  <p>Ім'я користувача: {{ username }}</p>
</template>

defineModel повертає ref, який залишається синхронізованим зі зв'язаним значенням батьківського компонента. Мутації цього ref автоматично випромінюють подію оновлення.

Готовий до співбесід з Vue.js / Nuxt.js?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Іменовані Моделі та Множинні Зв'язування v-model

Компоненти іноді потребують кількох двосторонніх зв'язувань. Vue підтримує іменовані зв'язування v-model, і defineModel обробляє їх, приймаючи аргумент імені.

vue
<!-- DateRangePicker.vue -->
<script setup lang="ts">
// Іменовані моделі для множинних зв'язувань v-model
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Валідація через computed
const isValidRange = computed(() => {
  if (!startDate.value || !endDate.value) return true
  return startDate.value <= endDate.value
})
</script>

<template>
  <div class="date-range-picker">
    <input
      type="date"
      :value="startDate?.toISOString().split('T')[0]"
      @input="startDate = new Date(($event.target as HTMLInputElement).value)"
    />
    <span>до</span>
    <input
      type="date"
      :value="endDate?.toISOString().split('T')[0]"
      @input="endDate = new Date(($event.target as HTMLInputElement).value)"
    />
    <p v-if="!isValidRange" class="error">Кінцева дата повинна бути пізніше початкової</p>
  </div>
</template>

Батьківський компонент зв'язує обидві моделі:

vue
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />

Модифікатори v-model з defineModel

Вбудовані модифікатори Vue, такі як .trim, .number та .lazy, працюють автоматично з defineModel. Користувацькі модифікатори потребують явної обробки через другий елемент поверненого кортежу.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Доступ до модифікаторів через друге повернене значення
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Функції трансформації для користувацьких модифікаторів
  set(value) {
    // Округлити до 2 десяткових знаків, якщо присутній модифікатор 'round'
    if (modifiers.round) {
      return Math.round(value * 100) / 100
    }
    return value
  }
})
</script>

<template>
  <div class="currency-input">
    <span>₴</span>
    <input
      type="number"
      step="0.01"
      :value="model"
      @input="model = parseFloat(($event.target as HTMLInputElement).value) || 0"
    />
  </div>
</template>

Використання з користувацьким модифікатором:

vue
<CurrencyInput v-model.round="price" />

Патерни TypeScript для Script Setup

Безпека типів у компонентах <script setup> базується на дженериках, що передаються до макросів компілятора. Патерн дещо відрізняється між props, emits та моделями.

vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'

// Props дженерик-компонента
const props = defineProps<{
  items: T[]
  selected?: T
}>()

const emit = defineEmits<{
  select: [item: T]
  remove: [id: string]
}>()

// Слоти також можна типізувати
defineSlots<{
  default: (props: { item: T; index: number }) => any
  empty: () => any
}>()

const hasItems = computed(() => props.items.length > 0)
</script>

<template>
  <ul v-if="hasItems">
    <li
      v-for="(item, index) in items"
      :key="item.id"
      :class="{ selected: selected?.id === item.id }"
      @click="emit('select', item)"
    >
      <slot :item="item" :index="index">
        {{ item.id }}
      </slot>
    </li>
  </ul>
  <div v-else>
    <slot name="empty">Немає елементів</slot>
  </div>
</template>

Дженерик-компоненти, доступні з Vue 3.3, дозволяють створювати повторно використовувані типізовані компоненти без втрати виведення типів у коді, що їх використовує. Для глибших патернів документація Vue про TypeScript охоплює граничні випадки.

Типові Питання на Співбесідах про Script Setup

Інтерв'юери, що тестують знання Vue, часто фокусуються на відмінностях між script setup та Options API, а також на механіці реактивності.

Питання: Чому деструктуризація props порушує реактивність і як Vue 3.5 це вирішує?

У JavaScript деструктуризація створює нові змінні, що містять значення на момент деструктуризації. Якщо props.count зміниться пізніше, деструктуризована const { count } = props все ще міститиме старе значення. Компілятор Vue 3.5 трансформує деструктуризовані змінні в геттери, які перевираховуються при зверненні, зберігаючи реактивність без накладних витрат під час виконання.

Питання: Що відбувається під капотом, коли використовується defineModel?

Компілятор розгортає defineModel в оголошення prop та реєстрацію emit. Під час виконання він створює ref, геттер якого читає значення prop, а сеттер викликає emit('update:modelValue', newValue). Цей ref не є справжньою копією prop, а проксі, яке відповідно перенаправляє читання та запис.

Питання: Коли компонент повинен використовувати script setup замість звичайної функції setup?

Script setup є кращим вибором для більшості однофайлових компонентів, оскільки він зменшує шаблонний код та покращує підтримку IDE. Звичайна функція setup() необхідна, коли компонент повинен експонувати методи батьківським компонентам через defineExpose у складних патернах, або коли створюються renderless-компоненти, що повертають функції рендерингу напряму. Для підготовки до співбесіди варто дослідити більше патернів Vue composables.

Міграція з Options API до Script Setup

Існуючі кодові бази часто містять компоненти Options API, які можуть виграти від міграції. Конвертація слідує передбачуваному патерну:

vue
<!-- До: Options API -->
<script lang="ts">
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  props: {
    items: { type: Array as PropType<string[]>, required: true }
  },
  emits: ['select'],
  data() {
    return { searchTerm: '' }
  },
  computed: {
    filteredItems() {
      return this.items.filter(item =>
        item.toLowerCase().includes(this.searchTerm.toLowerCase())
      )
    }
  },
  methods: {
    selectItem(item: string) {
      this.$emit('select', item)
    }
  }
})
</script>
vue
<!-- Після: Script Setup -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{ items: string[] }>()
const emit = defineEmits<{ select: [item: string] }>()

const searchTerm = ref('')

const filteredItems = computed(() =>
  props.items.filter(item =>
    item.toLowerCase().includes(searchTerm.value.toLowerCase())
  )
)

function selectItem(item: string) {
  emit('select', item)
}
</script>

Мігрована версія коротша, і виведення типів працює без перетворень PropType. Команда Vue надає vue-codemod для автоматичних міграцій, хоча ручний огляд залишається необхідним для складних компонентів.

Міркування щодо Продуктивності в Script Setup

Компоненти script setup компілюються в більш ефективний код, оскільки компілятор знає точну форму компонента під час збірки. Зокрема:

  • Зв'язування шаблону вирішуються під час компіляції, а не через runtime property lookup
  • Невикористані імпорти ефективніше видаляються через tree-shaking
  • Відсутність функції-обгортки зменшує накладні витрати на closure

Для застосунків, що пріоритизують розмір бандла, script setup з Vapor Mode (доступний у Vue 3.6+) повністю усуває віртуальний DOM для opt-in компонентів, генеруючи натомість прямі операції DOM.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Ключові Висновки для Vue 3 Script Setup та defineModel

  • Script setup зменшує шаблонний код компонента, автоматично експонуючи зв'язування верхнього рівня до шаблонів та обробляючи реєстрацію компонентів
  • defineModel замінює ручний патерн prop + emit для v-model, повертаючи ref, синхронізований зі зв'язаним значенням батьківського компонента
  • Vue 3.5 впроваджує деструктуризацію реактивних props, дозволяючи const { prop } = defineProps() без втрати реактивності
  • Іменовані моделі підтримують множинні зв'язування v-model на одному компоненті, використовуючи defineModel('name')
  • Дженерики TypeScript працюють з defineProps, defineEmits, defineSlots та дженерик-компонентами через атрибут generic
  • Міграція з Options API слідує механічному патерну: data стає refs, computed залишається computed, методи стають функціями, а компілятор обробляє експорти
Щоденний виклик

Чи знайдеш ти помилку в Vue.js / Nuxt.js?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 18 вересня 2026 р.

Теги

#vue
#vue3
#script-setup
#definemodel
#typescript
#composition-api

Поділитися

Пов'язані статті