ДокументацияКарьераСобеседование по Vue: реактивность, Composition API, компоненты
Средний 16 мин чтения

Собеседование по Vue: реактивность, Composition API, компоненты

Подготовка к Vue-собеседованию: типичные вопросы про реактивность, ref vs reactive, Composition API, lifecycle, компоненты, provide/inject, Pinia.

собеседованиеVueреактивностьComposition APIкомпонентыPiniainterviewвопросы

Базовые вопросы

Что такое Vue?

Прогрессивный фреймворк для построения UI. Ключевые особенности:

  • Реактивность — данные и UI синхронизируются автоматически
  • Компонентный подход — UI состоит из вложенных компонентов
  • Template syntax — декларативные шаблоны с директивами

Что такое <script setup>?

Синтаксический сахар для Composition API внутри однофайловых компонентов (SFC):

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

Все переменные и импорты в <script setup> автоматически доступны в шаблоне. Не нужно писать return.

Реактивность

Как работает реактивность во Vue?

Vue 3 использует Proxy для отслеживания изменений:

const state = reactive({ name: 'Анна', age: 25 })

// При обращении к state.name Vue «запоминает», какой компонент использует это свойство
// При изменении state.name Vue «уведомляет» компонент о перерисовке

Когда компонент рендерится, Vue записывает, какие реактивные свойства использовались. При изменении свойства — компонент обновляется.

Разница между ref и reactive?

// ref — для примитивов и любых значений
const count = ref(0)
const user = ref({ name: 'Анна' })
count.value++          // доступ через .value

// reactive — только для объектов
const state = reactive({ name: 'Анна', age: 25 })
state.age++            // прямой доступ, без .value
Свойствоrefreactive
ПримитивыДаНет
ОбъектыДа (через .value)Да
В шаблонеАвто-распаковкаПрямой доступ
Переприсвоениеref.value = newObjНельзя заменить целиком

Почему нельзя переприсвоить reactive?

const state = reactive({ name: 'Анна' })
state = reactive({ name: 'Иван' }) // Ошибка — потеряна связь Proxy

reactive возвращает Proxy-объект. Переприсвоение заменяет ссылку — реактивность теряется. Для замены объекта используйте ref или Object.assign:

Object.assign(state, { name: 'Иван' })

Что делает computed?

Создаёт вычисляемое значение с кэшированием:

const firstName = ref('Анна')
const lastName = ref('Иванова')

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

computed пересчитывается только когда меняются зависимости. В отличие от обычной функции, которая вызывается при каждом рендере.

Разница между computed и watch?

computed — вычисляет значение на основе других данных. watch — выполняет побочный эффект при изменении.

const search = ref('')

// computed — вернуть производное значение
const results = computed(() => items.filter(i => i.name.includes(search.value)))

// watch — сделать что-то при изменении (запрос на сервер, localStorage)
watch(search, (newVal) => {
  console.log('Поиск:', newVal)
})

Что делает watchEffect?

Как watch, но автоматически определяет зависимости:

const userId = ref(1)

watchEffect(() => {
  // Vue сам видит, что мы используем userId.value
  fetchUser(userId.value)
})

В отличие от watch, не нужно указывать, что отслеживать. Запускается сразу при создании и при каждом изменении реактивных данных внутри.

Компоненты

Как передать данные в дочерний компонент?

Через defineProps:

<!-- Child.vue -->
<script setup lang="ts">
const props = defineProps<{
  title: string
  count?: number
}>()
</script>

<template>
  <h1>{{ title }}</h1>
  <p>Счёт: {{ count ?? 0 }}</p>
</template>
<!-- Parent.vue -->
<Child title="Привет" :count="5" />

Как передать событие наверх?

Через defineEmits:

<!-- Child.vue -->
<script setup lang="ts">
const emit = defineEmits<{
  update: [value: string]
  delete: [id: number]
}>()

const handleChange = () => emit('update', 'новое значение')
</script>
<!-- Parent.vue -->
<Child @update="handleUpdate" @delete="handleDelete" />

Что такое v-model на компоненте?

Двусторонняя привязка — сокращение для :value + @input:

<!-- CustomInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>

<template>
  <input v-model="model" />
</template>
<!-- Parent.vue -->
<CustomInput v-model="username" />

Vue 3.4+: defineModel() автоматически создаёт prop и emit.

Что такое slots?

Слот — место в шаблоне, куда родитель передаёт контент:

<!-- Card.vue -->
<template>
  <div class="card">
    <slot name="header">Заголовок по умолчанию</slot>
    <slot />
    <slot name="footer" />
  </div>
</template>
<Card>
  <template #header>Мой заголовок</template>
  <p>Основной контент</p>
  <template #footer>Подвал</template>
</Card>

Что такое provide / inject?

Альтернатива props для глубоких вложенных компонентов:

<!-- Предок -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
</script>

<!-- Потомок (на любом уровне глубины) -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light') // 'light' — значение по умолчанию
</script>

Не нужно прокидывать props через каждый промежуточный компонент.

Lifecycle

Какие lifecycle-хуки есть?

onMounted(() => {
  // Компонент добавлен в DOM. Запросы данных, подписки.
})

onUpdated(() => {
  // Компонент перерисован. Осторожно — может вызвать бесконечный цикл.
})

onUnmounted(() => {
  // Компонент удалён. Очистка таймеров, отписка от событий.
})

onBeforeMount(() => { /* Перед добавлением в DOM */ })
onBeforeUpdate(() => { /* Перед перерисовкой */ })

Когда делать запрос в onMounted, а не в setup?

В onMounted — если нужны DOM-элементы или данные должны загружаться после начального рендера. В setup — если данные критичны для первого рендера и не зависят от DOM.

На практике большинство запросов данных делают в onMounted или в composables с onMounted.

Pinia

Что такое Pinia?

Официальное хранилище состояния для Vue (замена Vuex):

// stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const name = ref('Гость')
  const isLoggedIn = ref(false)

  const displayName = computed(() => isLoggedIn.value ? name.value : 'Гость')

  function login(userName: string) {
    name.value = userName
    isLoggedIn.value = true
  }

  function logout() {
    name.value = 'Гость'
    isLoggedIn.value = false
  }

  return { name, isLoggedIn, displayName, login, logout }
})
<script setup>
import { useUserStore } from '@/stores/user'

const user = useUserStore()
user.login('Анна')
</script>

В чём преимущество перед Vuex?

  • Нет mutations — действия синхронные и асинхронные в одном месте
  • TypeScript из коробки — не нужно возиться с типами
  • Меньше шаблонного кода
  • Можно несколько stores вместо одного огромного

Composables

Что такое composable?

Функция, инкапсулирующая реактивную логику для переиспользования:

// composables/useFetch.ts
export function useFetch<T>(url: string) {
  const data = ref<T | null>(null)
  const error = ref<string | null>(null)
  const loading = ref(true)

  const fetch_ = async () => {
    loading.value = true
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }

  onMounted(fetch_)

  return { data, error, loading, refresh: fetch_ }
}
<script setup>
const { data: users, loading } = useFetch<User[]>('/api/users')
</script>

Паттерны composables

// Мышь
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  onMounted(() => window.addEventListener('mousemove', handler))
  onUnmounted(() => window.removeEventListener('mousemove', handler))

  function handler(e: MouseEvent) {
    x.value = e.clientX
    y.value = e.clientY
  }

  return { x, y }
}

// localStorage
export function useLocalStorage(key: string, defaultValue: string) {
  const value = ref(localStorage.getItem(key) ?? defaultValue)

  watch(value, (newVal) => localStorage.setItem(key, newVal))

  return value
}

Директивы

Какие директивы часто спрашивают?

<p v-if="show">Показан</p>         <!-- Условный рендеринг (DOM убирается) -->
<p v-show="show">Показан</p>       <!-- CSS display: none -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
<input v-model="text" />           <!-- Двусторонняя привязка -->
<div :class="{ active: isActive }"> <!-- Динамический класс -->
<button @click="handler">Клик</button> <!-- Событие -->
<div v-html="htmlContent" />       <!-- Вывод HTML (осторожно: XSS) -->

Разница v-if и v-show?

v-if полностью убирает/добавляет элемент в DOM. v-show — переключает display: none.

Используйте v-if когда элемент редко показывается. v-show — когда переключается часто (меньше накладных расходов).

Производительность

Что делает defineAsyncComponent?

const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))

Загружает компонент лениво — только когда нужен. Снижает размер начального бандла.

Что делает <KeepAlive>?

Кэширует компонент вместо его уничтожения:

<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

При переключении вкладок компонент не теряет состояние. Полезно для форм, где пользователь уже ввёл данные.

Итог

  • Реактивность основана на Proxy — Vue автоматически отслеживает зависимости
  • ref — универсальный, reactive — только объекты, нельзя переприсвоить
  • computed — кэшированные вычисления, watch/watchEffect — побочные эффекты
  • <script setup> — стандартный синтаксис, не нужно писать return
  • Props вниз, emit вверх, provide/inject — для глубоких деревьев
  • Composables — главный паттерн переиспользования логики в Vue 3
  • Pinia — замена Vuex, меньше шаблонного кода