Перейти к содержимому

4. Options API

Прежде чем перейти к Composition API полностью, важно понять Options API. Почему? Потому что огромное количество существующего Vue кода написано на Options API, и тебе придётся его читать и поддерживать. Плюс, для небольших компонентов Options API всё ещё отлично работает! Не выбрасывай старый инструмент — просто знай, когда его применять. 🔧


<script lang="ts">
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
interface User {
id: number
name: string
}
export default defineComponent({
name: 'UserDashboard', // имя компонента (для DevTools)
// Дочерние компоненты
components: { UserCard, UserList },
// Директивы
directives: { focus: { mounted: el => el.focus() } },
// Props — входные данные
props: {
initialCount: {
type: Number,
default: 0
},
user: {
type: Object as PropType<User>,
required: true
}
},
// Events — исходящие события
emits: ['update', 'delete'],
// Реактивные данные
data() {
return {
count: this.initialCount, // можно использовать this.props!
message: 'Привет',
items: [] as string[]
}
},
// Вычисляемые свойства (кешируются!)
computed: {
doubled(): number {
return this.count * 2
},
fullMessage(): string {
return `${this.message}: ${this.count}`
}
},
// Наблюдатели за изменениями
watch: {
count(newVal: number, oldVal: number) {
console.log(`count: ${oldVal} → ${newVal}`)
}
},
// Методы
methods: {
increment() {
this.count++
this.$emit('update', this.count)
},
async loadData() {
this.items = await fetchItems()
}
},
// Lifecycle hooks
created() { /* Компонент создан, DOM ещё нет */ },
mounted() { this.loadData() },
updated() { /* DOM обновлён */ },
unmounted() { /* Компонент удалён */ }
})
</script>

export default defineComponent({
data() {
// ВАЖНО: возвращать новый объект для каждого экземпляра!
return {
// Примитивы
count: 0,
message: '',
isVisible: true,
// Объекты
user: {
name: 'Яша',
age: 25
},
// Массивы
items: ['Vue', 'React', 'Angular'],
// null для async данных
currentUser: null as User | null,
// Данные из props (в data() доступен this!)
localCount: this.initialCount
}
}
})

Важное правило: данные в data() должны быть объявлены сразу — добавление свойств позже не работает в реактивном смысле:

methods: {
// ❌ Не реактивно! Vue не отслеживает новые свойства
addNewField() {
this.newField = 'значение' // шаблон не обновится!
},
// ✅ Правильно — использовать Object.assign или деструктуризацию
updateUser() {
this.user = { ...this.user, newField: 'значение' }
}
}

export default defineComponent({
data() {
return {
firstName: 'Яша',
lastName: 'Смирнов',
items: [
{ id: 1, name: 'Vue', price: 0 },
{ id: 2, name: 'Vite', price: 0 },
{ id: 3, name: 'Pinia', price: 0 }
],
filterText: ''
}
},
computed: {
// Простое вычисление — только getter
fullName(): string {
return `${this.firstName} ${this.lastName}`
},
// Вычисление массива
filteredItems() {
return this.items.filter(item =>
item.name.toLowerCase().includes(this.filterText.toLowerCase())
)
},
// Computed с getter И setter
name: {
get(): string {
return `${this.firstName} ${this.lastName}`
},
set(value: string) {
const parts = value.split(' ')
this.firstName = parts[0] || ''
this.lastName = parts[1] || ''
}
}
},
methods: {
updateName() {
// Вызов setter computed
this.name = 'Иван Петров'
// Теперь: firstName = 'Иван', lastName = 'Петров'
}
}
})

Computed кешируются! Это ключевое отличие от methods:

computed: {
// ✅ Перевычисляется только когда expensiveList изменится
processedList() {
return this.expensiveList.map(item => heavyCalculation(item))
}
},
methods: {
// ❌ Вызывается каждый раз при рендере!
getProcessedList() {
return this.expensiveList.map(item => heavyCalculation(item))
}
}

export default defineComponent({
data() {
return {
searchQuery: '',
user: { name: 'Яша', settings: { theme: 'dark' } },
routeId: 1
}
},
watch: {
// Простой watcher
searchQuery(newVal: string, oldVal: string) {
console.log(`Поиск: "${oldVal}" → "${newVal}"`)
this.fetchResults(newVal)
},
// Watcher с опциями
routeId: {
immediate: true, // вызвать сразу при монтировании
handler(newId: number) {
this.fetchUserById(newId)
}
},
// Глубокий watcher для объектов
user: {
deep: true, // отслеживает вложенные изменения
handler(newUser: User) {
this.saveToStorage(newUser)
}
},
// Watcher на вложенное свойство
'user.settings.theme'(newTheme: string) {
document.body.className = newTheme
}
}
})

export default defineComponent({
// ─── ФАЗА СОЗДАНИЯ ──────────────────────────
beforeCreate() {
// Самый ранний hook. data и computed НЕ доступны
// Используется редко (Composition API setup() = beforeCreate + created)
console.log('beforeCreate: this.$data =', this.$data) // undefined
},
created() {
// data, computed, methods — ДОСТУПНЫ
// DOM ещё НЕ создан (this.$el = undefined)
// Идеально для fetch начальных данных
this.fetchInitialData()
},
// ─── ФАЗА МОНТИРОВАНИЯ ──────────────────────
beforeMount() {
// DOM создаётся, но ещё не вставлен в документ
// Используется очень редко
},
mounted() {
// ✅ DOM доступен! this.$el — HTML элемент
// Идеально для: DOM манипуляций, third-party библиотек, fetch
this.$el.querySelector('input')?.focus()
// Работа с реальным DOM через $refs
this.$refs.canvas.getContext('2d')
// Инициализация внешних библиотек
this.chart = new Chart(this.$refs.canvas, { ... })
},
// ─── ФАЗА ОБНОВЛЕНИЯ ────────────────────────
beforeUpdate() {
// Данные изменились, DOM ещё НЕ обновлён
// Можно получить старый DOM
console.log('DOM до обновления:', this.$el.innerHTML)
},
updated() {
// DOM обновлён!
// ⚠️ Не меняй данные здесь — бесконечный цикл!
console.log('DOM после обновления:', this.$el.innerHTML)
},
// ─── ФАЗА УДАЛЕНИЯ ──────────────────────────
beforeUnmount() {
// Компонент ещё живёт, но скоро будет удалён
// Cleanup: отписки, таймеры, внешние библиотеки
clearInterval(this.timer)
this.chart?.destroy()
window.removeEventListener('resize', this.handleResize)
},
unmounted() {
// Компонент удалён из DOM
console.log('Компонент удалён')
},
// ─── СПЕЦИАЛЬНЫЕ HOOKS ──────────────────────
activated() {
// При активации <KeepAlive> компонента
this.refreshData()
},
deactivated() {
// При деактивации <KeepAlive> компонента
},
errorCaptured(err, instance, info) {
// Перехватывает ошибки из дочерних компонентов
console.error(err)
return false // предотвращает дальнейшее распространение ошибки
}
})

Mixins — старый способ переиспользования 🧪

Заголовок раздела «Mixins — старый способ переиспользования 🧪»

Mixins — это способ переиспользовать Options API логику. В Vue 3 их заменили composables, но ты встретишь mixins в legacy коде:

mixins/timestampMixin.ts
export const timestampMixin = {
data() {
return {
createdAt: new Date(),
updatedAt: new Date()
}
},
methods: {
updateTimestamp() {
this.updatedAt = new Date()
}
}
}
// Использование
export default defineComponent({
mixins: [timestampMixin],
data() {
return {
name: 'Яша'
}
},
methods: {
save() {
this.updateTimestamp() // метод из mixin!
// сохраняем...
}
}
})

Проблемы mixins:

// ❌ Конфликт имён — непредсказуемо!
const mixin1 = { data() { return { value: 1 } } }
const mixin2 = { data() { return { value: 2 } } }
// Какой value победит? Зависит от порядка! 😱
defineComponent({ mixins: [mixin1, mixin2] })
// ❌ Неявный источник данных
// Откуда взялся this.someMethod()? Из mixin1? mixin2? Самого компонента?
// Невозможно понять без анализа всех mixins!
// ✅ Composables решают это явно
const { value: value1 } = useMixin1() // понятно откуда!
const { value: value2 } = useMixin2() // понятно откуда!

✅ Options API подходит для:
├── Простые компоненты (формы, карточки, кнопки)
├── Команда, переходящая с Vue 2
├── Быстрое прототипирование
└── Компоненты без сложной переиспользуемой логики
❌ Composition API лучше для:
├── Сложная бизнес-логика
├── Переиспользуемые composables
├── TypeScript строгая типизация
└── Крупные команды и проекты

Options APIComposition APIНазначение
data()ref(), reactive()Реактивные данные
computedcomputed()Вычисляемые свойства
watchwatch(), watchEffect()Наблюдатели
methodsОбычные функцииМетоды
createdsetup() (сразу)Инициализация
mountedonMounted()После монтирования
updatedonUpdated()После обновления
unmountedonUnmounted()После удаления
propsdefineProps<T>()Входные данные
emitsdefineEmits<T>()Исходящие события
mixinscomposablesПереиспользование
this.$refsref() для DOMДоступ к DOM
provide/injectprovide(), inject()DI

Доступ к экземпляру компонента через this ⚙️

Заголовок раздела «Доступ к экземпляру компонента через this ⚙️»

В Options API this — это экземпляр компонента с множеством встроенных свойств:

mounted() {
this.$el // корневой DOM элемент
this.$refs // template refs
this.$attrs // нереактивные атрибуты (fallthrough)
this.$slots // слоты
this.$emit // метод emit
this.$parent // родительский компонент (избегать!)
this.$root // корневой компонент
this.$router // Vue Router (если установлен)
this.$store // Vuex (если установлен)
this.$nextTick(() => { /* после обновления DOM */ })
this.$forceUpdate() // принудительное обновление (избегать!)
}

Options API — это:

  • data() — реактивные данные (объект из фабрики)
  • computed — кешированные вычисления
  • watch — наблюдатели с deep, immediate
  • methods — функции компонента
  • Lifecycle hooksmounted, unmounted и другие
  • Mixins — старый способ переиспользования (заменён composables)

Options API жив и поддерживается в Vue 3. Но для новых проектов — выбирай Composition API! ✅