Vue 3 Script Setup i defineModel w 2026: Nowoczesna Składnia i Pytania Rekrutacyjne

Kompleksowy przewodnik po składni script setup w Vue 3 oraz makrze defineModel. Omówienie reactive props destructure, typowania TypeScript oraz pytań rekrutacyjnych.

Vue 3 Script Setup i defineModel przewodnik

Składnia script setup w Vue 3 rewolucjonizuje sposób tworzenia komponentów, eliminując zbędny kod szablonowy i znacząco poprawiając integrację z TypeScript. W połączeniu z defineModel, wprowadzonym eksperymentalnie w Vue 3.3 i ustabilizowanym w Vue 3.4, budowanie komponentów z dwukierunkowym wiązaniem danych wymaga znacznie mniej kodu niż ekwiwalent w Options API.

Destrukturyzacja Props w Vue 3.5

Od Vue 3.5 właściwości (props) można destrukturyzować bezpośrednio z defineProps zachowując reaktywność. W większości przypadków eliminuje to potrzebę używania toRefs.

Podstawy Script Setup w Vue 3.5

Składnia <script setup> to cukier składniowy czasu kompilacji dla Composition API wewnątrz komponentów Single File. Zmienne zadeklarowane na najwyższym poziomie są automatycznie eksponowane do szablonu, a importy można używać bezpośrednio bez rejestracji.

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

// Props z interfejsem TypeScript
const props = defineProps<{
  user: User
  showEmail?: boolean
}>()

// Stan reaktywny
const isExpanded = ref(false)

// Właściwość computed
const displayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

// Emiter zdarzeń z typowanym payload
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// Metody to zwykłe funkcje
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 ? 'Zwiń' : 'Rozwiń' }}
    </button>
  </div>
</template>

Kompilator automatycznie obsługuje instrukcje eksportu i rejestrację komponentów. Redukuje to obciążenie poznawcze w porównaniu do jawnego zwracania wartości z funkcji setup().

Destrukturyzacja Reaktywnych Props (Vue 3.5+)

Przed Vue 3.5 destrukturyzacja props przerywała reaktywność, ponieważ destrukturyzacja w JavaScript tworzy migawkę wartości w danym momencie. Vue 3.5 wprowadza śledzenie na poziomie kompilatora, które zachowuje reaktywność dla destrukturyzowanych props.

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

// Destrukturyzacja z domyślnymi wartościami - pozostaje reaktywna w 3.5+
const { query = '', placeholder = 'Szukaj...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch działa na destrukturyzowanych props
watch(
  () => query,
  (newQuery) => {
    console.log('Zapytanie się zmieniło:', newQuery)
  }
)
</script>

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

Ta składnia wymaga Vue 3.5 lub nowszego. Dokumentacja Vue 3.5 release announcement opisuje pełne zachowanie destrukturyzacji reaktywnych props.

defineModel dla Dwukierunkowego Wiązania

Przed defineModel implementacja v-model na własnym komponencie wymagała deklaracji prop i ręcznego emitowania zdarzenia aktualizacji. Ten wzorzec występuje w niezliczonych bazach kodu Vue:

vue
<!-- Stary sposób - rozwlekły ale jawny -->
<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>

Z defineModel ten sam komponent staje się znacznie krótszy:

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel tworzy ref synchronizowany z v-model
const model = defineModel<string>({ default: '' })
</script>

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

Komponent nadrzędny używa standardowej składni 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>Nazwa użytkownika: {{ username }}</p>
</template>

defineModel zwraca ref, który pozostaje zsynchronizowany z wartością powiązaną przez rodzica. Mutacje tego ref automatycznie emitują zdarzenie aktualizacji.

Gotowy na rozmowy o Vue.js / Nuxt.js?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Nazwane Modele i Wiele Wiązań v-model

Komponenty czasami potrzebują wielu dwukierunkowych wiązań. Vue obsługuje nazwane wiązania v-model, a defineModel obsługuje je poprzez przyjmowanie argumentu nazwy.

vue
<!-- DateRangePicker.vue -->
<script setup lang="ts">
// Nazwane modele dla wielu wiązań v-model
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Walidacja 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>do</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">Data końcowa musi być późniejsza niż początkowa</p>
  </div>
</template>

Komponent nadrzędny wiąże oba modele:

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

Modyfikatory v-model z defineModel

Wbudowane modyfikatory Vue jak .trim, .number i .lazy działają automatycznie z defineModel. Niestandardowe modyfikatory wymagają jawnej obsługi przez drugi element zwracanej krotki.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Dostęp do modyfikatorów przez drugą zwracaną wartość
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Funkcje transformacji dla niestandardowych modyfikatorów
  set(value) {
    // Zaokrąglij do 2 miejsc po przecinku jeśli modyfikator 'round' jest obecny
    if (modifiers.round) {
      return Math.round(value * 100) / 100
    }
    return value
  }
})
</script>

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

Użycie z niestandardowym modyfikatorem:

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

Wzorce TypeScript dla Script Setup

Bezpieczeństwo typów w komponentach <script setup> opiera się na generykach przekazywanych do makr kompilatora. Wzorzec różni się nieznacznie między props, emits i modelami.

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

// Props generycznego komponentu
const props = defineProps<{
  items: T[]
  selected?: T
}>()

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

// Sloty też mogą być typowane
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">Brak elementów</slot>
  </div>
</template>

Komponenty generyczne, dostępne od Vue 3.3, umożliwiają tworzenie wielokrotnego użytku typowanych komponentów bez poświęcania wnioskowania typów w kodzie konsumującym. Dla głębszych wzorców dokumentacja Vue o TypeScript omawia przypadki brzegowe.

Typowe Pytania Rekrutacyjne o Script Setup

Rekruterzy testujący wiedzę o Vue często koncentrują się na różnicach między script setup a Options API, jak również na mechanice reaktywności.

Pytanie: Dlaczego destrukturyzacja props przerywa reaktywność i jak Vue 3.5 to rozwiązuje?

W JavaScript destrukturyzacja tworzy nowe zmienne przechowujące wartości w momencie destrukturyzacji. Jeśli props.count zmieni się później, destrukturyzowana const { count } = props nadal przechowuje starą wartość. Kompilator Vue 3.5 przekształca destrukturyzowane zmienne w gettery, które ponownie ewaluują przy dostępie, zachowując reaktywność bez narzutu runtime.

Pytanie: Co dzieje się pod spodem gdy używane jest defineModel?

Kompilator rozwija defineModel w deklarację prop i rejestrację emit. W runtime tworzy ref, którego getter odczytuje wartość prop, a setter wywołuje emit('update:modelValue', newValue). Ten ref nie jest prawdziwą kopią prop, ale proxy, które odpowiednio przekierowuje odczyty i zapisy.

Pytanie: Kiedy komponent powinien używać script setup zamiast zwykłej funkcji setup?

Script setup jest preferowany dla większości komponentów single-file, ponieważ redukuje kod szablonowy i poprawia wsparcie IDE. Zwykła funkcja setup() jest niezbędna gdy komponent musi eksponować metody do komponentów nadrzędnych przez defineExpose w złożonych wzorcach, lub gdy budowane są komponenty renderless zwracające funkcje render bezpośrednio. Do przygotowania do rozmowy rekrutacyjnej warto poznać więcej wzorców Vue composables.

Migracja z Options API do Script Setup

Istniejące bazy kodu często zawierają komponenty Options API, które mogą skorzystać z migracji. Konwersja podąża za przewidywalnym wzorcem:

vue
<!-- Przed: 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
<!-- Po: 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>

Zmigrowana wersja jest krótsza i wnioskowanie typów działa bez rzutowań PropType. Zespół Vue dostarcza vue-codemod do automatycznych migracji, choć ręczny przegląd pozostaje konieczny dla złożonych komponentów.

Rozważania Wydajnościowe w Script Setup

Komponenty script setup kompilują się do bardziej wydajnego kodu, ponieważ kompilator zna dokładny kształt komponentu w czasie budowania. Konkretnie:

  • Wiązania szablonu rozwiązywane są w czasie kompilacji zamiast runtime property lookup
  • Nieużywane importy są efektywniej usuwane przez tree-shaking
  • Brak opakowującej funkcji redukuje narzut closure

Dla aplikacji priorytetyzujących rozmiar bundle, script setup z Vapor Mode (dostępnym w Vue 3.6+) eliminuje wirtualny DOM całkowicie dla opted-in komponentów, generując bezpośrednie operacje DOM.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Kluczowe Wnioski dla Vue 3 Script Setup i defineModel

  • Script setup redukuje kod szablonowy komponentu automatycznie eksponując wiązania najwyższego poziomu do szablonów i obsługując rejestrację komponentów
  • defineModel zastępuje ręczny wzorzec prop + emit dla v-model, zwracając ref synchronizowany z wartością powiązaną przez rodzica
  • Vue 3.5 wprowadza destrukturyzację reaktywnych props, pozwalając na const { prop } = defineProps() bez utraty reaktywności
  • Nazwane modele obsługują wiele wiązań v-model na pojedynczym komponencie używając defineModel('name')
  • Generyki TypeScript działają z defineProps, defineEmits, defineSlots i generycznymi komponentami przez atrybut generic
  • Migracja z Options API podąża za mechanicznym wzorcem: data staje się refs, computed pozostaje computed, metody stają się funkcjami, a kompilator obsługuje eksporty
Wyzwanie dnia

Znajdziesz błąd w Vue.js / Nuxt.js?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 18 września 2026

Tagi

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

Udostępnij

Powiązane artykuły