Vue
Пользовательские директивы
Выпадающее меню должно закрываться по клику в любом месте за его пределами. Через обычные обработчики это означает повесить слушатель на document в onMounted, проверять, попал ли клик внутрь элемента, и не забыть снять слушатель в onUnmounted - и так в каждом компоненте с выпадашкой. Пользовательская директива v-click-outside сворачивает всю эту работу с DOM в одно слово в шаблоне. Директивы существуют ровно для того, что компонент выразить не может: прямое низкоуровневое поведение конкретного элемента.
- v-focus: автофокус на поле при появлении, например на поиске в модалке
- v-click-outside: закрытие выпадающего меню или поповера по клику снаружи
- v-tooltip: показ подсказки при наведении без обёртки элемента в компонент
- Интеграция jQuery-плагинов или библиотек анимации, требующих прямого доступа к DOM-узлу
Предварительные знания
- Шаблонный синтаксис и привязка через v-bind
- Понимание DOM-элемента и событий на нём
- Знакомство с жизненным циклом на уровне идеи монтирования и размонтирования
Что такое пользовательская директива
Кроме встроенных директив (v-if, v-for, v-model) можно объявить свои. Пользовательская директива - это объект с функциями-хуками, каждая из которых получает сам DOM-элемент. Это даёт прямой низкоуровневый доступ к элементу, чего компоненты намеренно не предоставляют. Локальную директиву объявляют в script setup переменной с префиксом v, и она доступна в шаблоне того же компонента.
Имя переменной обязано начинаться с v и далее идти в camelCase: vFocus в коде превращается в v-focus в шаблоне. Глобальную директиву регистрируют через app.directive('focus', {...}), и тогда она доступна во всех компонентах приложения.
Директивы стоит держать как последнее средство для прямого DOM-доступа. Если задачу можно выразить компонентом или composable, обычно так понятнее: директива работает в обход декларативной модели и её сложнее отлаживать.
Что отличает пользовательскую директиву от компонента?
Хуки директивы
Директива объявляет хуки, повторяющие жизненный цикл элемента. mounted вызывается, когда элемент вставлен в DOM. updated - после обновления содержащего его компонента. beforeUnmount - перед удалением элемента, и это место для очистки: снять слушатели, остановить таймеры. Есть также created и beforeMount для ранних стадий, но на практике чаще нужны mounted и beforeUnmount.
| Хук | Когда вызывается | Типичная задача |
|---|---|---|
| mounted | Элемент вставлен в DOM | Подписка, фокус, init библиотеки |
| updated | Компонент обновился | Реакция на смену значения директивы |
| beforeUnmount | Перед удалением элемента | Очистка: снять слушатели, остановить таймеры |
Парность mounted и beforeUnmount - тот же принцип, что у onMounted и onUnmounted в компоненте. Подписку, поставленную в mounted, обязательно снимают в beforeUnmount, иначе слушатель на document утечёт.
В каком хуке директивы снимают слушатель события, добавленный в mounted?
Аргументы, модификаторы и значение
Директива принимает данные тремя способами, и все они приходят во втором параметре хука - объекте binding. Значение передаётся присваиванием (v-pin="200") и доступно как binding.value. Аргумент после двоеточия (v-pin:top) приходит в binding.arg. Модификаторы после точки (v-pin.smooth) собираются в binding.modifiers как объект флагов.
- binding.value - значение справа от знака равенства (здесь 80)
- binding.oldValue - предыдущее значение, доступно в хуке updated
- binding.arg - аргумент после двоеточия (здесь 'top')
- binding.modifiers - объект флагов после точек (здесь { smooth: true })
Аргумент может быть динамическим: v-pin:[side]="80", где side - реактивная переменная. Это позволяет менять поведение директивы на лету, не переписывая шаблон под каждый случай.
В записи v-pin:top.smooth="80" что попадёт в binding.arg, binding.modifiers и binding.value?
Когда директива, а когда компонент
Директива оправдана, когда нужно низкоуровневое поведение конкретного DOM-элемента: фокус, клик вне, наблюдатель пересечений, интеграция сторонней библиотеки по узлу. Если же задача про разметку, состояние и логику данных, почти всегда уместнее компонент или composable. Директива работает в обход декларативной модели, поэтому злоупотребление ею делает код труднее для понимания.
- Директива — Прямое поведение одного DOM-элемента: фокус, click-outside, tooltip, lazy-load изображения
- Компонент — Разметка, состояние, переиспользуемый блок интерфейса с собственным шаблоном
| Задача | Инструмент |
|---|---|
| Автофокус на поле при появлении | Директива v-focus |
| Закрытие по клику снаружи | Директива v-click-outside |
| Карточка с заголовком и слотами | Компонент |
| Переиспользуемая логика загрузки данных | Composable |
Проверочный вопрос: нужен ли прямой доступ к DOM-узлу. Если да и это про поведение элемента - директива. Если задача описывается через состояние и разметку - компонент. Если это переиспользуемая логика без привязки к конкретному узлу - composable.
Для какой из задач пользовательская директива - более подходящий инструмент, чем компонент?
Связь с другими темами
Директивы дают низкоуровневый доступ к DOM там, где компонента и composable мало:
- Хуки жизненного цикла — Хуки директивы повторяют идею mounted и beforeUnmount, но привязаны к одному элементу
- Composables — Оба приёма инкапсулируют логику с очисткой; выбор зависит от того, нужен ли доступ к DOM-узлу
Итог
- Пользовательская директива - объект с хуками, получающими сам DOM-элемент для прямого управления им
- Основные хуки: mounted (элемент в DOM), updated (после обновления), beforeUnmount (перед удалением, для очистки)
- Аргумент передаётся через двоеточие (v-pin:top), модификаторы через точку (v-pin.smooth), значение через присваивание
- Все данные приходят в объект binding: value, oldValue, arg, modifiers
- Директивы нужны для низкоуровневого поведения DOM; для логики с состоянием обычно лучше компонент или composable
Связанные уроки
- vue-11-lifecycle — Хуки директивы (mounted, beforeUnmount) повторяют идею жизненного цикла, но для отдельного DOM-элемента
- vue-12-composables — И директива, и composable инкапсулируют логику с очисткой, но директива работает на уровне DOM-элемента