Options API та composition API
Options API організовує Vue-компоненти через іменовані властивості об'єкта, наприклад data і methods. Composition API групує логіку за функціональністю через імпортовані функції всередині setup().
Теорія
TL;DR
- Options API = картотека з підписаними ящиками (дані тут, методи там, computed окремо)
- Composition API = дошка зі стікерами, згрупованими за завданням (логіка лічильника в одному блоці)
- Головна різниця: Options розкидає пов'язаний код по всьому об'єкту; Composition тримає його разом
- Маленький компонент або прототип? Options підійде. Повторне використання логіки або 500+ рядків? Composition
- Проект із TypeScript? Composition дає кращий вивід типів через
defineProps<{...}>()
Швидкий приклад
Options API розкидає логіку лічильника по секціях:
<script>
export default {
data() { return { count: 0 }; },
computed: { double() { return this.count * 2; } },
methods: { increment() { this.count++; } }
};
</script>
<!-- data, computed і methods у трьох окремих секціях -->
<!-- хоча всі вони належать одній фічі - лічильнику -->Composition API тримає ту саму логіку разом:
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
</script>
<!-- вся логіка лічильника в одному місці -->Результат ідентичний. Різниця в тому, де шукати пов'язаний код, коли відкриваєш файл.
Ключова різниця
Options API структурує код за типом: реактивні дані в data(), функції в methods, похідні значення в computed. Стан, методи і computed лічильника живуть у трьох різних секціях. Composition API структурує за призначенням: все що потрібно лічильнику знаходиться в одному блоці. На практиці перехід зазвичай відбувається після третього разу, коли гортаєш файл на 400 рядків у пошуках методу, що змінює конкретний шматок стану.
Коли використовувати
- Новачок або простий UI-віджет: Options. Структура передбачувана, не потрібно перебудовувати мислення.
- Повторне використання логіки в кількох компонентах: Composition. Виносьте в composable на зразок
useCounter()абоuseFetch(). - Компонент виростає більше 300-500 рядків: Composition. Постійно стрибати між
data,methodsіcomputedзаради однієї фічі стомлює. - Проект із TypeScript: Composition.
<script setup>автоматично виводить типи пропсів черезdefineProps<{ name: string }>(). Options потребує громіздкого злиття декларацій (declaration merging) черезComponentOptions. - Vue 2 або міграція: Options. Пряма сумісність без зайвих пакетів.
- Pinia або Nuxt 3 composables: Composition. Обидва побудовані навколо цієї моделі.
Таблиця порівняння
| Аспект | Options API | Composition API |
|---|---|---|
| Структура | За типом опції (data/methods/computed) | За фічею (логіка лічильника разом) |
| Реактивність | this.count (відстежується автоматично) | ref(0) або reactive({}), потрібен .value |
| Повторне використання | Копіювати методи по компонентах вручну | Composables (useCounter(), useFetch()) |
| TypeScript | Громіздкі ComponentOptions декларації | Вивід типів у <script setup> |
| Розмір файлу | Компактний для малих компонентів | Добре масштабується для великих файлів |
| Коли використовувати | Прототипи, Vue 2, прості віджети | Продакшн, спільна логіка, TypeScript |
Як це працює всередині
Vue 3 парсить Options API в proxy-обернутий контекст this і автоматично прив'язує реактивність через Object.defineProperty на властивості data під час ініціалізації компонента. Composition API запускає setup() першим, збираючи проксі ref і reactive у спільний контекст рендеру. Планувальник залежностей під час компіляції шаблону працює однаково в обох випадках. Структурна різниця в тому, що логіка Composition залишається в ланцюжках областей видимості JavaScript, а не висить на this.
Типові помилки
Мутація ref без .value:
const count = ref(0);
count++; // залишається 0, збільшено об'єкт-обгортку, а не значенняref повертає { value: 0 }. Пряма мутація обходить реактивність. Правило: count.value++.
Звернення до this всередині setup():
export default {
setup() {
console.log(this.count); // undefined
}
}setup() запускається до того, як proxy this існує. Немає до чого прив'язуватися. Повертайте refs зі setup, або використовуйте <script setup>.
Припущення, що reactive відстежує глибоко вкладені додавання:
const state = reactive({ items: [] });
state.items.push({ count: 0 }); // { count: 0 } НЕ є реактивнимreactive обгортає тільки об'єкт верхнього рівня. Об'єкти, що додаються пізніше, потребують власного ref або reactive. Правило: state.items.push({ count: ref(0) }).
Змішування обох API в одному компоненті:
export default {
data() { return { x: 0 }; },
setup() { return { x: ref(0) }; } // Options x перекриває setup x
}Затінення. Властивості Options перекривають повернуті зі setup значення з однаковим ім'ям. Обирайте одне API на компонент.
Де зустрічається
- Nuxt 3: Composition скрізь.
useFetch,useRoute,useAsyncData- усе composables. - Pinia:
defineStoreвикористовує Composition-стиль ізrefіcomputedвсередині. - VitePress:
<script setup>у всіх інтерактивних demo-прикладах документації. - Element Plus: Складна логіка мігрує на Composition для підтримки tree-shaking.
- Vuetify: Options для базових компонентів, Composition для кастомної логіки плагінів.
Follow-up питання
Q: Що таке composable?
A: Функція, яка всередині використовує Composition API і повертає реактивний стан або методи. Приклад: function useCounter() { const count = ref(0); return { count, increment: () => count.value++ }; }. Аналог кастомних хуків у React.
Q: Яка різниця між ref і reactive?
A: ref працює для примітивів і об'єктів, у шаблонах розпаковується автоматично, але в скрипті потрібен .value. reactive тільки для об'єктів, дозволяє прямий доступ без .value, але втрачає реактивність при деструктуризації. ref для count, reactive для { user, settings }.
Q: Чи можна використовувати Options API у Vue 3?
A: Так. Options API повністю підтримується у Vue 3, команда Vue не планує його прибирати. Composition API - доповнення, а не заміна.
Q: Чому Composition API краще для TypeScript?
A: <script setup> із defineProps<{ name: string }>() виводить типи безпосередньо з дженерика. Options потребує громіздкого злиття декларацій через ComponentOptions. Тому TypeScript-команди зазвичай обирають Composition.
Q: Переведіть цей Options-компонент на Composition. (Senior)
A: Властивості data стають ref або reactive. Властивості computed стають викликами computed(). Методи стають звичайними функціями. Хуки на зразок mounted стають onMounted. Використовуйте <script setup>, щоб уникнути явного return.
Приклади
Базовий лічильник: Options проти Composition
Options API розбиває лічильник по трьох секціях:
<template>
<p>Counter: {{ count }} | Double: {{ double }}</p>
<button @click="increment">+</button>
</template>
<script>
export default {
data() {
return { count: 0 };
},
computed: {
double() { return this.count * 2; }
},
methods: {
increment() { this.count++; }
}
};
</script>Composition API тримає ту саму логіку разом:
<template>
<p>Counter: {{ count }} | Double: {{ double }}</p>
<button @click="increment">+</button>
</template>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
</script>Рендер ідентичний. У Options-версії для розуміння лічильника потрібно читати три окремих секції. У Composition - чотири послідовних рядки.
Список задач із фільтрацією
Ось де різниця стає відчутною. Options розкидає логіку фільтрації по файлу:
<script>
export default {
data() {
return {
todos: [],
filter: 'all' // стан тут
};
},
computed: {
filteredTodos() { // похідна логіка далеко від стану
return this.todos.filter(t =>
this.filter === 'all' || t.done === (this.filter === 'done')
);
}
},
methods: {
addTodo(text) { // мутації ще далі
this.todos.push({ text, done: false });
}
}
};
</script>Composition тримає все разом і спрощує вилучення:
<script setup>
import { ref, computed } from 'vue';
// Вся логіка задач в одному блоці, легко перенести в useTodos.js
const todos = ref([]);
const filter = ref('all');
const filteredTodos = computed(() =>
todos.value.filter(t =>
filter.value === 'all' || t.done === (filter.value === 'done')
)
);
const addTodo = (text) => todos.value.push({ text, done: false });
</script>Потрібно поділитися логікою з іншим компонентом? Берете блок і переносите в useTodos.js. З Options доведеться копіювати з трьох різних місць.
Патерн composable
Справжня цінність у продакшн. Виносимо в useTodos.js:
// useTodos.js
import { ref, computed } from 'vue';
export function useTodos() {
const todos = ref([]);
const filter = ref('all');
const filteredTodos = computed(() =>
todos.value.filter(t =>
filter.value === 'all' || t.done === (filter.value === 'done')
)
);
const addTodo = (text) => todos.value.push({ text, done: false });
return { todos, filter, filteredTodos, addTodo };
}Будь-який компонент імпортує це двома рядками:
<script setup>
import { useTodos } from './useTodos';
const { todos, filter, filteredTodos, addTodo } = useTodos();
</script>У Options API немає рівнозначного патерну. Міксини (mixins) існували, але проблеми з колізією імен і непрозорими джерелами даних робили їх болючими. Composables замінили їх повністю.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.