Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Обчислені, методи та спостерігачі у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Computed, methods і watchers** - три інструменти Vue для реактивних даних. Computed кешує похідні значення і перераховується тільки при зміні залежностей. Methods виконуються при кожному виклику без кешу. Watchers запускають callback при зміні джерела, призначені для async side effects. ```javascript const count = ref(0) const doubled = computed(() => count.value * 2) // кешовано watch(count, (newVal) => fetchData(newVal)) // side effect ``` **Головне:** computed для відображення, methods для обробників подій з параметрами, watchers для API-запитів і side effects.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 ### Таблиця порівняння | Властивість | Computed | Methods | Watchers | |---|---|---|---| | **Кешування** | Так, залежності | Ні | Ні | | **Повертає значення** | Завжди | Опційно | Ні | | **Тригер** | Читання + зміна залежностей | Кожен виклик | Зміна джерела | | **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` прив'язаний до одного поля.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.