Skip to main content

Обчислені, методи та спостерігачі у Vue.js

Computed, methods і watchers - три інструменти Vue для роботи з реактивними даними: computed кешує похідні значення і перераховується тільки при зміні залежностей, methods виконуються при кожному виклику без кешу, watchers запускають side effects при зміні конкретного джерела.

Теорія

TL;DR

  • Computed = формула в Excel: перераховується тільки коли змінюються вхідні дані
  • Methods = кнопка калькулятора: виконується заново при кожному виклику, без пам'яті
  • Watchers = спостерігач: викликає callback коли конкретний ref чи reactive змінюється
  • Потрібне відображення значення? Computed. Обробка події з параметрами? Method. Запит до API при зміні? Watcher.
  • Computed тільки синхронний; watchers підтримують async (API-запити, localStorage, debounce)

Швидкий приклад

vue
<template> <button @click="count++">Кліки: {{ count }}</button> <p>Подвоєне (computed): {{ doubleCount }}</p> <p>Подвоєне (method): {{ doubleCountMethod() }}</p> </template> <script setup> import { ref, computed, watch } from 'vue' const count = ref(0) const doubleCount = computed(() => { console.log('Computed запустився') // Один раз на зміну count return count.value * 2 }) const doubleCountMethod = () => { console.log('Method запустився') // При кожному рендері return count.value * 2 } watch(count, (newVal) => { console.log('Watcher спрацював:', newVal) // При кожній зміні count }) </script>

Клікни кнопку і подивись в консоль: "Computed запустився" з'являється один раз на клік, "Method запустився" при кожному циклі рендеру, watcher спрацьовує при кожній зміні count.

Головна різниця

Computed відстежує реактивні залежності під час першого запуску і кешує результат. Система реактивності Vue (Proxy у Vue 3) запам'ятовує до яких refs звертався геттер, і скидає кеш тільки коли вони змінюються. Methods - це звичайні функції без будь-якого відстеження. Watchers використовують явне джерело і запускають callbacks асинхронно через планувальник Vue, тобто виконуються після того як Vue завершить поточний цикл оновлення DOM. Саме тому watchers підходять для side effects: API-запитів, запису в localStorage, роботи з DOM.

Коли що використовувати

  • Відображення похідних даних (fullName з firstName + lastName): computed
  • Обробка подій користувача з параметрами (submit форми, клік на кнопку): method
  • Реакція на зміну даних з API-запитом або записом у localStorage: watcher
  • Читаєш одне й те саме значення кілька разів за один рендер: computed, не method
  • Потрібно стежити за кількома реактивними джерелами в одному callback: watcher з масивом джерел
  • Одноразове обчислення з параметром: method

Таблиця порівняння

ВластивістьComputedMethodsWatchers
КешуванняТак, залежностіНіНі
Повертає значенняЗавждиОпційноНі
ТригерЧитання + зміна залежностейКожен викликЗміна джерела
AsyncНі (тільки синхронно)ТакТак
Side effectsНе рекомендуєтьсяДопустимоДля цього й призначений
Найкраще дляВідображення в шаблоніОбробники подій, параметризовані викликиAPI-запити, debounce, localStorage

Як Vue обробляє це всередині

Vue 3 огортає refs і reactive-об'єкти у Proxy. Коли геттер computed запускається вперше, Vue активує Effect і записує кожне реактивне значення до якого звертається код, через track(). При зміні залежності Effect позначається застарілим, а геттер перезапускається при наступному читанні - не одразу. Тому computed ледачий і кешований. Methods обходять цей механізм повністю. Watchers використовують той самий Effect, але з явним джерелом і асинхронним flush через queueScheduler.

Типові помилки

1. Method для значень які повторюються в шаблоні

vue
<!-- getFullName() виконується при кожному рендері --> <template> <p>{{ getFullName() }}</p> <p>{{ getFullName() }}</p> </template> <!-- computed виконується один раз при зміні залежностей --> <template> <p>{{ fullName }}</p> <p>{{ fullName }}</p> </template> <script setup> import { ref, computed } from 'vue' const firstName = ref('Олена') const lastName = ref('Коваль') const fullName = computed(() => `${firstName.value} ${lastName.value}`) </script>

На списку з 1000 елементів різниця відчутна. Computed потрібен коли одне похідне значення з'являється в шаблоні кілька разів.

2. Async у computed

vue
<script setup> import { ref, computed, watchEffect } from 'vue' const userId = ref(1) const user = ref(null) // Неправильно: computed має бути синхронним const userName = computed(async () => { user.value = await fetchUser(userId.value) // Повертає Promise, не ім'я return user.value.name }) // Правильно: watchEffect для async watchEffect(async () => { if (userId.value) { user.value = await fetchUser(userId.value) } }) </script>

Computed поверне Promise-об'єкт, а не resolved значення. Vue не чекає на нього. Ця помилка часто зустрічається в Nuxt.js auth-модулях.

3. Side effects у computed

vue
<script setup> // Неправильно: side effect при кожному читанні геттера const bad = computed(() => { apiCall() // Якщо шаблон читає bad 5 разів, apiCall спрацює 5 разів return count.value * 2 }) // Правильно watch(count, () => { apiCall() }) </script>

4. Watcher на нереактивне значення

vue
<script setup> let plainVar = 'hello' // Неправильно: plainVar не реактивний, watcher ніколи не спрацює watch(plainVar, (newVal) => console.log(newVal)) // Правильно: загорни в ref або передай функцію-геттер const reactiveVar = ref('hello') watch(reactiveVar, (newVal) => console.log(newVal)) </script>

5. Deep watcher на великих об'єктах

vue
// Неправильно: рекурсивно сканує весь об'єкт при кожній мутації watch(items, cb, { deep: true }) // Краще: стежи тільки за тим що потрібно watch(() => items.value.length, cb)

{ deep: true } на масиві з 500 елементів запускає повний рекурсивний обхід при кожній вкладеній зміні. Звужуй до конкретного поля де можливо.

Де зустрічається в реальних проектах

  • Pinia/Vuex store: getters - це computed властивості (відфільтровані задачі, підсумок кошика)
  • Vue Storefront: total обчислюється з масиву елементів кошика, кешується між рендерами
  • Nuxt.js: watchers на параметри роуту для запуску data fetch при навігації
  • Element Plus: computed для динамічних повідомлень валідації форм
  • Vuetify: methods для параметризованих обробників кнопок

Питання для поглиблення

Q: Чому computed не може бути асинхронним?
A: Computed вимагає синхронного значення на виході. Повернення Promise дає Vue Promise-об'єкт, а не дані. Для async логіки використовуй watchEffect або watch з async callback.

Q: Як Vue визначає залежності computed властивості?
A: Під час першого запуску геттера Vue активує Effect і викликає track() для кожного Proxy до якого звертається код. Вони стають залежностями. Коли будь-яка з них змінюється, trigger() позначає computed застарілим.

Q: Яка різниця між watch і watchEffect?
A: watchEffect запускається одразу, автоматично збирає залежності зі свого тіла і перезапускається при їх зміні. watch явний: ти вказуєш джерело, він ледачий за замовчуванням, і callback отримує старе і нове значення.

Q: Коли computed скидає кеш для об'єктних залежностей?
A: За замовчуванням Vue відстежує тільки поверхневий рівень: саме посилання на об'єкт. Мутація вкладеної властивості без заміни ref не скидає computed. Використовуй reactive або перебудуй так, щоб змінювалося саме посилання.

Q: (Senior) Якщо реалізувати мемоїзовану функцію через замикання (closure), чого їй бракує порівняно з Vue computed?
A: Планувальника. Computed Vue групує анулювання і відкладає перезапуск до наступного тіку, уникаючи зайвих обчислень коли кілька залежностей змінюються в одній мікрозадачі. Звичайне замикання перезапускається одразу при кожній зміні без групування.

Приклади

Підсумок кошика з computed

vue
<script setup> import { ref, computed } from 'vue' const items = ref([ { name: 'Футболка', price: 300, qty: 2 }, { name: 'Штани', price: 600, qty: 1 } ]) const total = computed(() => { // Перераховується тільки коли змінюється масив items або його елементи return items.value.reduce((sum, item) => sum + item.price * item.qty, 0) }) </script> <template> <ul> <li v-for="item in items" :key="item.name"> {{ item.name }} x{{ item.qty }} = {{ item.price * item.qty }} грн </li> </ul> <p>Разом: {{ total }} грн</p> </template>

total читається один раз за цикл рендеру незалежно від кількості звернень у шаблоні. Змінюєш qty - Vue перераховує, але тільки тоді. Саме цей паттерн використовується у Vue Storefront.

Пошук з debounced watcher

vue
<script setup> import { ref, watch } from 'vue' const query = ref('') const results = ref([]) let timer = null watch(query, (newVal) => { clearTimeout(timer) timer = setTimeout(async () => { if (newVal.trim()) { results.value = await searchAPI(newVal) } else { results.value = [] } }, 300) }) async function searchAPI(q) { const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`) return res.json() } </script> <template> <input v-model="query" placeholder="Введіть для пошуку..." /> <ul> <li v-for="r in results" :key="r.id">{{ r.title }}</li> </ul> </template>

Це саме той паттерн для якого призначені watchers: async side effect з debounce, який не блокує рендер і не повертає значення в шаблон. Computed тут не підійде.

Writable computed для двостороннього зв'язку

vue
<script setup> import { ref, computed } from 'vue' const firstName = ref('Олена') const lastName = ref('Коваль') const fullName = computed({ get() { return `${firstName.value} ${lastName.value}` }, set(newValue) { const parts = newValue.split(' ') firstName.value = parts[0] ?? '' lastName.value = parts[1] ?? '' } }) </script> <template> <input v-model="fullName" /> <p>Ім'я: {{ firstName }}, Прізвище: {{ lastName }}</p> </template>

Writable computed зустрічається рідше, але корисний коли батьківський компонент працює з об'єднаним значенням, а стан зберігається окремо. Бачив такий підхід у бібліотеках форм де адреса зберігається по частинах, але v-model прив'язаний до одного поля.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?