Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Користувацькі директиви у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Користувацькі директиви у Vue.js** - функції для прямої маніпуляції DOM-елементами в шаблонах. Підходять для imperativних задач (фокус, скрол, ініціалізація сторонніх бібліотек) де компоненти або composables були б надлишковими. Дані передаються через `binding.value`, Vue сам викликає хуки `mounted`, `updated`, `unmounted`. ```typescript const vAutoFocus = { mounted(el: HTMLElement) { el.focus() } } // Використання: <input v-auto-focus /> ``` **Головне:** директиви для прямого доступу до DOM, composables для реактивної логіки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Користувацькі директиви у Vue.js** - це функції, які ти реєструєш для прямої маніпуляції DOM-елементами в шаблонах, минаючи компонентну логіку для imperativних задач: управління фокусом, ініціалізація сторонніх бібліотек, виявлення кліку за межами елемента. ## Теорія ### TL;DR - Директиви схожі на спеціалізовані кухонні гаджети: вбудовані Vue (`v-if`, `v-for`) вирішують типові задачі, ти пишеш свої для специфічної роботи з DOM. - Головна різниця: компоненти управляють даними та UI декларативно, директиви безпосередньо змінюють реальний DOM-елемент. - Дані передаються через `binding.value` (як `v-tooltip="'Зберегти'"`), Vue викликає хуки автоматично. - Використовуй коли потрібен прямий доступ до елемента (фокус, скрол, сторонні бібліотеки). - Для реактивного стану і бізнес-логіки - composables, не директиви. ### Швидкий приклад ```vue <script setup lang="ts"> // Локальна директива - Vue розпізнає її по префіксу 'v' const vAutoFocus = { mounted(el: HTMLElement) { el.focus() // Викликається один раз, одразу після вставки в DOM } } </script> <template> <!-- Інпут отримує фокус одразу при монтуванні - без ref --> <input v-auto-focus placeholder="Цей елемент фокусується при завантаженні" /> </template> ``` Директива підключається до lifecycle Vue і запускає `focus()` прямо на елементі. Без `ref`, без `onMounted` всередині компонента. ### Ключова відмінність Компоненти живуть в реактивному світі: вони описують як має виглядати UI залежно від стану. Директиви знаходяться на рівень нижче. Вони отримують реальний DOM-вузол і діють на нього безпосередньо. Дані передаєш через `binding.value`, але сама директива не має стану компонента і не має `this`. По суті це функція, яка запускається на елементі в конкретні lifecycle-моменти. ### Коли використовувати - **Imperativні DOM-операції** (фокус, `scrollIntoView`, `select()`) там де метод компонента додав би зайвий код. - **Інтеграція сторонніх бібліотек**: Tippy.js, clipboard API, чарти, яким потрібен реальний DOM-вузол для ініціалізації. - **Поведінка елемента, що повторюється**: скорочення тексту, lazy-load зображень, виявлення кліку поза межами. - **Уникай** коли потрібен реактивний стан, обчислювані значення або взаємодія між компонентами - для цього є composables. ### Як Vue обробляє директиви Компілятор шаблонів сканує твій `<template>` під час білду і перетворює `v-myDir` в об'єкт `vnode.directive`, прикріплений до віртуального вузла. В рантаймі під час циклу `patch()` (diff та оновлення Vue 3) Vue викликає твої хук-функції на реальному DOM `el`, передаючи `binding` (value, arg, modifiers), `vnode` і `prevVnode`. Все виконується в головному потоці браузера, в тому ж планувальнику реактивності що й компоненти. ### Об'єкт binding ```typescript // Шаблон: <div v-my-directive:arg.mod1.mod2="someValue"> // binding містить: { value: someValue, // Що ти передав у шаблоні oldValue: ..., // Попереднє значення, доступне в хуку updated arg: 'arg', // Рядок після двокрапки modifiers: { mod1: true, mod2: true }, instance: ..., // Екземпляр компонента dir: ..., // Сам об'єкт визначення директиви } ``` ### Lifecycle-хуки директиви ```typescript const myDirective = { created(el, binding, vnode) {}, // До застосування атрибутів елемента beforeMount(el, binding, vnode) {}, // До вставки елемента в DOM mounted(el, binding, vnode) {}, // Після вставки елемента в DOM beforeUpdate(el, binding, vnode, prevVnode) {}, // До оновлення батьківського компонента updated(el, binding, vnode, prevVnode) {}, // Після оновлення батьківського компонента beforeUnmount(el, binding, vnode) {}, // До видалення елемента unmounted(el, binding, vnode) {}, // Після видалення елемента } ``` На практиці більшість директив, які тобі доведеться писати, використовують лише три хуки: `mounted`, `updated` і `unmounted`. Решта існує для крайніх випадків. ### Типові помилки **Не очищати в `unmounted`:** ```typescript // Неправильно: слухач подій займає пам'ять при кожній навігації mounted(el) { el.addEventListener('click', handler) } // Немає unmounted - після 1000 навігацій в SPA буде 1000 слухачів на document // Правильно: mounted(el) { el.addEventListener('click', handler) }, unmounted(el) { el.removeEventListener('click', handler) } ``` **Спроба звернутися до стану компонента безпосередньо:** ```typescript // Неправильно: у директив немає 'this', немає стану зі setup mounted(el, binding) { this.myData = 'value' // ReferenceError в strict mode } // Правильно: передавай дані через binding.value // <div v-my-dir="myReactiveValue"> mounted(el, binding) { doSomething(binding.value) } ``` **Запускати важкі операції при кожному оновленні батька:** ```typescript // Неправильно: ініціалізує бібліотеку при кожному перерендері updated(el, binding) { initExpensiveLib(el, binding.value) } // Правильно: пропускай якщо значення не змінилось updated(el, binding) { if (binding.value !== binding.oldValue) { initExpensiveLib(el, binding.value) } } ``` **Глобальна реєстрація без префіксу:** ```typescript // Ризик: конфлікт з майбутніми вбудованими директивами Vue app.directive('focus', vFocus) // Безпечніше: app.directive('app-focus', vFocus) ``` ### Де зустрічається - **VueUse** надає 50+ директив: `vAutoResize`, `vInfiniteScroll` - використовуються в PrimeVue та Quasar. - **Element Plus** - директива `v-loading` для оверлеїв зі спінером в корпоративних дашбордах. - **Nuxt UI** - `v-motion` для анімацій без обгортання кожного елемента в окремий компонент. - **Глобальна реєстрація** в `main.ts` через `app.directive()` для директив що потрібні по всьому додатку. - **Локальна реєстрація** в `<script setup>` - будь-яка змінна з префіксом `v` (наприклад `vAutoFocus`) автоматично стає директивою. ### Питання на співбесіді **Q:** Який порядок виклику хуків під час оновлення компонента? **A:** Спочатку `beforeUpdate`, потім `updated`. Для вкладених елементів хуки виконуються знизу вгору для дочірніх і зверху вниз для батьківських. **Q:** Як модифікатори (modifiers) директив працюють зсередини? **A:** Компілятор парсить `v-dir.foo.bar` і передає `{ foo: true, bar: true }` як `binding.modifiers`. У хуку ти читаєш це як звичайний об'єкт. Динамічні аргументи `v-dir:[dynamicArg]` резолвляться в рядок під час виконання. **Q:** Яка різниця між локальною і глобальною реєстрацією директиви? **A:** Локальна - оголошуєш змінну з `v` в `<script setup>`, доступна тільки в цьому компоненті. Глобальна - `app.directive('name', def)` в `main.ts`, доступна скрізь, але додає ім'я в простір імен додатку. **Q:** Чи може директива звертатися до екземпляра компонента? **A:** Так, через `binding.instance`. Але це створює тісне зв'язування між директивою і конкретною формою компонента. Майже завжди краще передавати дані через `binding.value`. **Q:** (Senior) Як оптимізувати директиву що використовується на 1000+ елементах списку? **A:** Пропускай хуки коли `binding.value === binding.oldValue`. Застосовуй debounce для важких DOM-операцій. Зберігай мутабельний стан прямо на `el` (наприклад `el._cleanup`) замість зовнішніх Map, які тримають посилання і заважають GC. Уникай `getCurrentInstance()` всередині директив - це вимикає tree-shaking для всього модуля. ## Приклади ### Виявлення кліку за межами елемента Найпоширеніший реальний юз-кейс - закривати дропдауни або модалки при кліку в іншому місці. ```typescript // directives/clickOutside.ts export const vClickOutside = { mounted(el: HTMLElement, binding: { value: () => void }) { el._clickOutsideHandler = (event: MouseEvent) => { if (!el.contains(event.target as Node)) { binding.value() // Викликаємо обробник переданий з шаблону } } document.addEventListener('click', el._clickOutsideHandler) }, unmounted(el: HTMLElement) { // Це і є головний сенс - без цього рядка ти маєш витік пам'яті document.removeEventListener('click', el._clickOutsideHandler) }, } ``` ```vue <template> <div v-click-outside="closeDropdown" class="dropdown"> <!-- Вміст дропдауну --> </div> </template> ``` Очищення в `unmounted` не є опціональним. Без нього кожна навігація в SPA тихо додає новий слухач на `document`. ### Tooltip з Tippy.js Директиви - правильна абстракція для обгортання imperativних API сторонніх бібліотек. ```vue <script setup lang="ts"> import tippy from 'tippy.js' const vTooltip = { mounted(el: HTMLElement, binding) { tippy(el, { content: binding.value, trigger: 'hover focus' }) }, updated(el: HTMLElement, binding) { // Оновлюємо контент реактивно - без повного перемонтування el._tippy?.setContent(binding.value) }, unmounted(el: HTMLElement) { el._tippy?.destroy() } } </script> <template> <button v-tooltip="tooltipText">Зберегти</button> </template> ``` Це паттерн з бібліотек типу VueUse. Директива приховує складний imperativний API Tippy і дає чистий декларативний інтерфейс. Коли `tooltipText` змінюється, спрацьовує `updated` і `setContent` оновлює вміст без знищення і створення інстансу заново. ### Ліниве завантаження зображень з IntersectionObserver ```typescript // directives/lazyLoad.ts export const vLazyLoad = { mounted(el: HTMLImageElement, binding: { value: string }) { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { el.src = binding.value // Завантажуємо лише коли елемент видимий observer.disconnect() // Зупиняємо спостереження після першого завантаження } }) observer.observe(el) }, } ``` ```vue <!-- Реєструємо глобально в main.ts, далі: --> <img v-lazy-load="imageUrl" alt="Фото продукту" /> ``` Кожен екземпляр директиви отримує власний observer, який відключається після завантаження зображення. Жодних зайвих мережевих запитів для контенту нижче видимої частини сторінки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.