Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Основні директиви у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Директиви Vue.js** - це атрибути з префіксом `v-`, що додають реактивну поведінку DOM прямо в шаблоні. | Директива | Що робить | |---|---| | `v-if` / `v-else` | Умовно додає або видаляє елемент з DOM | | `v-show` | Перемикає `display: none` | | `v-for` | Рендерить список (завжди з `:key`) | | `v-bind` / `:` | Прив'язує атрибути динамічно | | `v-on` / `@` | Додає обробники подій | | `v-model` | Двостороння прив'язка для форм | **Головне правило:** `v-if` видаляє вузол з DOM, `v-show` ховає через CSS. До `v-for` завжди додавай `:key`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Директиви Vue.js** - це атрибути з префіксом `v-`, які роблять DOM-елементи реактивними прямо в шаблоні. Замість того щоб вручну викликати `document.querySelector` і перемикати класи, ти оголошуєш поведінку, а Vue сам оновлює DOM. ## Теорія ### TL;DR - Директиви - це як стікери на HTML: `v-if` видаляє елемент коли умова хибна, `v-for` створює по одному елементу на кожен запис масиву - Шість основних: `v-if/v-else` для умов, `v-for` для списків, `v-bind` (`:`) для атрибутів, `v-on` (`@`) для подій, `v-model` для форм, `v-show` для перемикання через CSS - `v-if` знищує DOM-вузол і запускає lifecycle hooks; `v-show` просто ставить `display: none` - До `v-for` завжди додавай `:key` зі стабільним унікальним значенням, не індексом масиву ### Швидкий приклад ```vue <template> <p v-if="show">Visible!</p> <p v-else>Hidden.</p> <ul> <li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li> </ul> </template> <script setup> import { ref } from 'vue' const show = ref(true) // зміни на false - абзаци поміняються місцями const fruits = ref(['Apple', 'Banana']) </script> ``` Коли `show` дорівнює `true`, перший абзац рендериться, а другого в DOM немає взагалі. Зміни `show` на `false` і Vue поміняє їх місцями. Список відображає по одному `<li>` на елемент, а `:key="fruit"` дозволяє Vue відстежувати кожен окремо. ### Шість директив **v-if / v-else / v-else-if** додають або видаляють елементи залежно від умови. Коли `v-if` стає хибним, Vue викликає `unmounted` на всіх компонентах всередині та видаляє вузол. Коли умова знову стає true, Vue створює свіжий вузол і запускає `mounted`. ```vue <p v-if="isLoggedIn">Welcome!</p> <p v-else>Please log in.</p> ``` **v-show** керує видимістю через `display: none`. Вузол залишається в DOM, lifecycle hooks не спрацьовують. Тому `v-show` дешевший для елементів, які часто перемикаються. **v-for** рендерить список, ітеруючись по масивах або об'єктах. Атрибут `:key` не опціональний. Без нього Vue перевикористовує DOM-вузли за позицією, і будь-яке переупорядкування дає неправильний стан, збиті поля вводу та перемішаний стан компонентів. ```vue <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li> ``` **v-bind** (скорочено `:`) прив'язує JavaScript-значення до HTML-атрибута. `:src="imageUrl"` ідентично `v-bind:src="imageUrl"`. Працює з будь-яким атрибутом: `class`, `style`, `disabled`, `href`, кастомними data-атрибутами. **v-on** (скорочено `@`) додає обробники подій. `@click="handler"` замінює `addEventListener('click', handler)` на цьому елементі. Вбудовані вирази теж працюють: `@click="count++"`. **v-model** забезпечує двосторонню прив'язку (two-way binding) для полів форм. Під капотом це `:value="text" @input="text = $event.target.value"`. Для кастомних компонентів у Vue 3 директива прив'язується до пропу `modelValue` і слухає подію `update:modelValue`. ### v-if vs v-show: коли що обирати | | v-if | v-show | |---|---|---| | DOM-вузол | Видаляється і відтворюється | Завжди присутній | | `display: none` | Ні | Так, коли прихований | | Lifecycle hooks | Так | Ні | | Підходить для | Умов, що рідко змінюються | Частого показу та приховування | Одного разу я бачив команду, яка використовувала `v-if` для спінера завантаження, що перемикався 10 разів на секунду в polling-циклі. Кожен тогл був повним знищенням і відтворенням DOM. Перехід на `v-show` скоротив витрати до однієї операції зі стилем. ### Як директиви працюють всередині Компілятор шаблонів Vue читає атрибути `v-` під час збірки і перетворює їх на функції рендерингу (render functions). Ці функції повертають VNode - звичайні JavaScript-об'єкти, що описують DOM. Під час виконання Vue загортає значення `ref()` в ES6 `Proxy`. Коли функція рендерингу зчитує реактивне значення, Proxy записує залежність. При зміні значення Vue ставить рендер у чергу, порівнює нове дерево VNode зі старим і патчить лише змінені вузли. Оновлення батчуються на тік мікрозадачі. Тобто якщо ти зміниш три `ref`-и в одній функції, Vue виконає один DOM-патч, а не три. ### Типові помилки **Індекс масиву як `:key` у v-for** ```vue <!-- Неправильно: індекс не працює при переупорядкуванні --> <li v-for="(item, index) in items" :key="index">{{ item.name }}</li> <!-- Правильно: стабільний унікальний id --> <li v-for="item in items" :key="item.id">{{ item.name }}</li> ``` Коли елементи переупорядковуються, ключі-індекси змушують Vue зіставляти DOM-вузли з неправильними даними. Поля вводу втрачають фокус, анімації відтворюються на неправильному елементі, стан компонентів перемішується. **Очікування v-else після v-for** ```vue <!-- Неправильно: Vue видасть помилку --> <li v-for="item in items">{{ item }}</li> <p v-else>Немає елементів.</p> <!-- Правильно: обгорни в контейнер з v-if --> <template v-if="items.length"> <li v-for="item in items" :key="item.id">{{ item }}</li> </template> <p v-else>Немає елементів.</p> ``` `v-else` повинен бути прямим сусідом одиночного `v-if`-елемента, а не `v-for`. **v-if і v-for на одному елементі** ```vue <!-- Уникай: у Vue 3 v-if виконується першим і не має доступу до змінних v-for --> <li v-for="item in items" v-if="item.active" :key="item.id">{{ item.name }}</li> <!-- Краще: фільтруй через computed --> <li v-for="item in activeItems" :key="item.id">{{ item.name }}</li> ``` Відфільтруй масив через computed ref і ітеруйся по результату. ### Де застосовується - Nuxt.js: `v-if="status === 'pending'"` показує спінер під час async-завантаження сторінок - Vuetify: `v-for="item in menuItems" :key="item.title"` на `<v-list-item>` в навігаційних drawer-ах - Quasar: `v-model` на `<q-input>` для двосторонньої прив'язки в PWA-формах - Pinia: `v-for="todo in store.todos"` тягне дані зі стору безпосередньо в шаблон ### Питання на співбесіді **Q:** Яка різниця між `v-if` і `v-show`? **A:** `v-if` видаляє вузол з DOM коли умова хибна і відтворює його коли стає true. `v-show` залишає вузол в DOM і перемикає `display: none`. Для частих переключень `v-show` дешевший. Для контенту, який з'являється рідко, `v-if` краще - вузол взагалі не існує коли прихований. **Q:** Чому `:key` важливий у `v-for` і чому індекс - поганий ключ? **A:** Ключі дозволяють Vue зіставляти DOM-вузли з конкретними записами даних. Без них Vue матчить за позицією. Індекс погоний з тієї ж причини: якщо видалити або переупорядкувати елементи, індекси зсуваються і Vue зіставляє неправильні вузли. **Q:** Чи можна поставити `v-if` і `v-for` на один елемент? **A:** Можна, але краще уникати. У Vue 3 `v-if` обчислюється першим і не має доступу до змінних `v-for`. Відфільтруй масив через computed ref і ітеруйся по ньому. **Q:** Як `v-model` працює під капотом? **A:** Це скорочення для `:value + @input` на нативних полях вводу. Для кастомних компонентів у Vue 3 прив'язується до пропу `modelValue` і слухає подію `update:modelValue`. **Q:** (Senior) Як Vue знає яку директиву оновлювати при зміні реактивного значення? **A:** Vue загортає реактивні дані в ES6 `Proxy`. Коли директива зчитує реактивне значення під час рендерингу, Proxy записує залежність. При зміні значення Vue позначає відповідні вотчери як застарілі і ставить рендер у чергу. Реальний DOM-патч відбувається на наступному тіку мікрозадачі - всі синхронні мутації збираються в один прохід. ## Приклади ### Базовий: перемикання стану авторизації ```vue <template> <button @click="isLoggedIn = !isLoggedIn"> {{ isLoggedIn ? 'Logout' : 'Login' }} </button> <p v-if="isLoggedIn">Welcome, user!</p> <p v-else>Please sign in first.</p> </template> <script setup> import { ref } from 'vue' const isLoggedIn = ref(false) </script> ``` Текст кнопки оновлюється реактивно. Абзаци міняються місцями на кожному кліку. Компонент управляє одним булевим значенням, Vue обробляє всі DOM-зміни. ### Середній: список задач з API ```vue <template> <ul> <li v-for="todo in todos" :key="todo.id" @click="todo.done = !todo.done" > {{ todo.text }} <span v-if="todo.done">✓</span> </li> </ul> </template> <script setup> import { ref, onMounted } from 'vue' const todos = ref([]) onMounted(async () => { const res = await fetch('/api/todos') todos.value = await res.json() }) </script> ``` Після монтування задачі завантажуються з API і `v-for` їх рендерить. Клік по рядку перемикає `done`. Vue оновлює тільки `<span>` для конкретного елемента, не перерендерюючи весь список. ### Просунутий: вкладені v-for зі стабільними ключами ```vue <template> <div v-for="user in users" :key="user.id"> <strong>{{ user.name }}</strong> <ul> <li v-for="task in user.tasks" :key="task.id">{{ task.text }}</li> </ul> </div> </template> <script setup> import { ref } from 'vue' const users = ref([ { id: 1, name: 'Alice', tasks: [{ id: 1, text: 'Design review' }, { id: 2, text: 'Write tests' }] }, { id: 2, name: 'Bob', tasks: [{ id: 1, text: 'Deploy staging' }] } ]) </script> ``` Обидва цикли використовують `id` як ключ. Якщо до списку задач Alice додається новий елемент, Vue вставляє рівно один `<li>`, не торкаючись рядка Bob. Без `task.id` як ключа будь-яка мутація масиву викликає повне переузгодження списку за позицією.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.