Директивы — специальные атрибуты с префиксом v-, дающие Vue-компилятору инструкции по управлению DOM. Встроенные директивы: v-if, v-for, v-model, v-show.
Кастомные директивы позволяют инкапсулировать логику работы с DOM-элементами в переиспользуемую единицу.
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})<script setup></script>
<template>
<input v-focus />
</template>app.directive('my-directive', {
// Перед вставкой элемента в DOM
beforeMount(el, binding, vnode) {},
// После вставки элемента в DOM (самый частый)
mounted(el, binding, vnode) {},
// Перед обновлением компонента
beforeUpdate(el, binding, vnode, prevVnode) {},
// После обновления компонента и дочерних
updated(el, binding, vnode, prevVnode) {},
// Перед размонтированием
beforeUnmount(el, binding, vnode) {},
// После размонтирования
unmounted(el, binding, vnode) {},
})<!-- v-директива:аргумент.модификатор="значение" -->
<div v-highlight:background.animate="'#ff0000'"></div>app.directive('highlight', {
mounted(el, binding) {
console.log(binding.value) // '#ff0000' — переданное значение
console.log(binding.arg) // 'background' — аргумент
console.log(binding.modifiers) // { animate: true } — модификаторы
console.log(binding.oldValue) // предыдущее значение (в updated)
console.log(binding.instance) // экземпляр компонента
}
})app.directive('focus', {
mounted(el) {
el.focus()
}
})app.directive('tooltip', {
mounted(el, { value }) {
const tip = document.createElement('div')
tip.className = 'tooltip'
tip.textContent = value
el._tooltip = tip
el.addEventListener('mouseenter', () => {
document.body.appendChild(tip)
const rect = el.getBoundingClientRect()
tip.style.top = rect.top - tip.offsetHeight - 8 + 'px'
tip.style.left = rect.left + 'px'
})
el.addEventListener('mouseleave', () => tip.remove())
},
updated(el, { value }) {
el._tooltip.textContent = value
},
unmounted(el) {
el._tooltip?.remove()
// важно: убрать event listeners
}
})app.directive('click-outside', {
mounted(el, { value }) {
el._clickOutside = (event) => {
if (!el.contains(event.target)) {
value(event) // value — callback функция
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
})<div v-click-outside="closeMenu">Меню</div><!-- Передаём объект, если нужно несколько параметров -->
<div v-tooltip="{ text: 'Подсказка', position: 'top', delay: 300 }">
Наведи на меня
</div>Система директив через data-атрибуты и MutationObserver — аналог того, как Vue применяет директивы к DOM
// Мини-система директив: регистрируем директивы по имени,
// применяем к элементам через data-атрибуты.
// MutationObserver следит за появлением новых элементов.
class DirectiveSystem {
constructor() {
this.directives = new Map()
this._observer = null
}
// Зарегистрировать директиву
directive(name, def) {
this.directives.set(name, def)
return this
}
// Применить директивы к элементу
applyToElement(el) {
for (const [name, def] of this.directives) {
const attrName = `data-v-${name}`
if (el.hasAttribute && el.hasAttribute(attrName)) {
const rawValue = el.getAttribute(attrName)
const binding = {
value: this._parseValue(rawValue),
arg: el.getAttribute(`data-v-${name}-arg`) || null,
modifiers: this._parseModifiers(el, name),
}
if (def.mounted) {
def.mounted(el, binding)
}
}
}
}
_parseValue(raw) {
if (!raw) return null
try { return JSON.parse(raw) } catch { return raw }
}
_parseModifiers(el, name) {
const mods = {}
const prefix = `data-v-${name}-mod-`
if (el.getAttributeNames) {
for (const attr of el.getAttributeNames()) {
if (attr.startsWith(prefix)) {
mods[attr.slice(prefix.length)] = true
}
}
}
return mods
}
// Обновить значение директивы
update(el, name, newValue) {
const def = this.directives.get(name)
if (!def || !def.updated) return
const binding = {
value: newValue,
oldValue: this._parseValue(el.getAttribute(`data-v-${name}`)),
arg: null,
modifiers: {},
}
el.setAttribute(`data-v-${name}`, JSON.stringify(newValue))
def.updated(el, binding)
}
// Отключить директиву
unmount(el, name) {
const def = this.directives.get(name)
if (def && def.unmounted) {
def.unmounted(el)
}
}
}
// --- Регистрируем директивы ---
const system = new DirectiveSystem()
// v-highlight: задаёт фоновый цвет
system.directive('highlight', {
mounted(el, binding) {
el.style = el.style || {}
el.style.backgroundColor = binding.value || '#yellow'
el.style.padding = '4px 8px'
el.style.borderRadius = '3px'
console.log(`[v-highlight] mounted: backgroundColor = ${binding.value}`)
},
updated(el, binding) {
el.style.backgroundColor = binding.value
console.log(`[v-highlight] updated: ${binding.oldValue} -> ${binding.value}`)
},
unmounted(el) {
el.style.backgroundColor = ''
console.log('[v-highlight] unmounted: стиль сброшен')
}
})
// v-badge: добавляет счётчик
system.directive('badge', {
mounted(el, binding) {
el._badge = `[${binding.value}]`
el.textContent = (el.textContent || '') + ' ' + el._badge
console.log(`[v-badge] mounted: badge = ${el._badge}`)
},
updated(el, binding) {
const oldBadge = `[${binding.oldValue}]`
const newBadge = `[${binding.value}]`
el.textContent = (el.textContent || '').replace(oldBadge, newBadge)
el._badge = newBadge
console.log(`[v-badge] updated: ${oldBadge} -> ${newBadge}`)
}
})
// --- Фейковый DOM-объект ---
const fakeEl = {
attributes: { 'data-v-highlight': '#3498db', 'data-v-badge': '5' },
style: {},
textContent: 'Кнопка',
hasAttribute(name) { return name in this.attributes },
getAttribute(name) { return this.attributes[name] ?? null },
getAttributeNames() { return Object.keys(this.attributes) },
setAttribute(name, val) { this.attributes[name] = val },
}
console.log('=== Применяем директивы ===')
system.applyToElement(fakeEl)
console.log('\n=== Обновляем директивы ===')
system.update(fakeEl, 'highlight', '#e74c3c')
system.update(fakeEl, 'badge', 12)
console.log('\n=== Размонтируем ===')
system.unmount(fakeEl, 'highlight')Директивы — специальные атрибуты с префиксом v-, дающие Vue-компилятору инструкции по управлению DOM. Встроенные директивы: v-if, v-for, v-model, v-show.
Кастомные директивы позволяют инкапсулировать логику работы с DOM-элементами в переиспользуемую единицу.
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})<script setup></script>
<template>
<input v-focus />
</template>app.directive('my-directive', {
// Перед вставкой элемента в DOM
beforeMount(el, binding, vnode) {},
// После вставки элемента в DOM (самый частый)
mounted(el, binding, vnode) {},
// Перед обновлением компонента
beforeUpdate(el, binding, vnode, prevVnode) {},
// После обновления компонента и дочерних
updated(el, binding, vnode, prevVnode) {},
// Перед размонтированием
beforeUnmount(el, binding, vnode) {},
// После размонтирования
unmounted(el, binding, vnode) {},
})<!-- v-директива:аргумент.модификатор="значение" -->
<div v-highlight:background.animate="'#ff0000'"></div>app.directive('highlight', {
mounted(el, binding) {
console.log(binding.value) // '#ff0000' — переданное значение
console.log(binding.arg) // 'background' — аргумент
console.log(binding.modifiers) // { animate: true } — модификаторы
console.log(binding.oldValue) // предыдущее значение (в updated)
console.log(binding.instance) // экземпляр компонента
}
})app.directive('focus', {
mounted(el) {
el.focus()
}
})app.directive('tooltip', {
mounted(el, { value }) {
const tip = document.createElement('div')
tip.className = 'tooltip'
tip.textContent = value
el._tooltip = tip
el.addEventListener('mouseenter', () => {
document.body.appendChild(tip)
const rect = el.getBoundingClientRect()
tip.style.top = rect.top - tip.offsetHeight - 8 + 'px'
tip.style.left = rect.left + 'px'
})
el.addEventListener('mouseleave', () => tip.remove())
},
updated(el, { value }) {
el._tooltip.textContent = value
},
unmounted(el) {
el._tooltip?.remove()
// важно: убрать event listeners
}
})app.directive('click-outside', {
mounted(el, { value }) {
el._clickOutside = (event) => {
if (!el.contains(event.target)) {
value(event) // value — callback функция
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
})<div v-click-outside="closeMenu">Меню</div><!-- Передаём объект, если нужно несколько параметров -->
<div v-tooltip="{ text: 'Подсказка', position: 'top', delay: 300 }">
Наведи на меня
</div>Система директив через data-атрибуты и MutationObserver — аналог того, как Vue применяет директивы к DOM
// Мини-система директив: регистрируем директивы по имени,
// применяем к элементам через data-атрибуты.
// MutationObserver следит за появлением новых элементов.
class DirectiveSystem {
constructor() {
this.directives = new Map()
this._observer = null
}
// Зарегистрировать директиву
directive(name, def) {
this.directives.set(name, def)
return this
}
// Применить директивы к элементу
applyToElement(el) {
for (const [name, def] of this.directives) {
const attrName = `data-v-${name}`
if (el.hasAttribute && el.hasAttribute(attrName)) {
const rawValue = el.getAttribute(attrName)
const binding = {
value: this._parseValue(rawValue),
arg: el.getAttribute(`data-v-${name}-arg`) || null,
modifiers: this._parseModifiers(el, name),
}
if (def.mounted) {
def.mounted(el, binding)
}
}
}
}
_parseValue(raw) {
if (!raw) return null
try { return JSON.parse(raw) } catch { return raw }
}
_parseModifiers(el, name) {
const mods = {}
const prefix = `data-v-${name}-mod-`
if (el.getAttributeNames) {
for (const attr of el.getAttributeNames()) {
if (attr.startsWith(prefix)) {
mods[attr.slice(prefix.length)] = true
}
}
}
return mods
}
// Обновить значение директивы
update(el, name, newValue) {
const def = this.directives.get(name)
if (!def || !def.updated) return
const binding = {
value: newValue,
oldValue: this._parseValue(el.getAttribute(`data-v-${name}`)),
arg: null,
modifiers: {},
}
el.setAttribute(`data-v-${name}`, JSON.stringify(newValue))
def.updated(el, binding)
}
// Отключить директиву
unmount(el, name) {
const def = this.directives.get(name)
if (def && def.unmounted) {
def.unmounted(el)
}
}
}
// --- Регистрируем директивы ---
const system = new DirectiveSystem()
// v-highlight: задаёт фоновый цвет
system.directive('highlight', {
mounted(el, binding) {
el.style = el.style || {}
el.style.backgroundColor = binding.value || '#yellow'
el.style.padding = '4px 8px'
el.style.borderRadius = '3px'
console.log(`[v-highlight] mounted: backgroundColor = ${binding.value}`)
},
updated(el, binding) {
el.style.backgroundColor = binding.value
console.log(`[v-highlight] updated: ${binding.oldValue} -> ${binding.value}`)
},
unmounted(el) {
el.style.backgroundColor = ''
console.log('[v-highlight] unmounted: стиль сброшен')
}
})
// v-badge: добавляет счётчик
system.directive('badge', {
mounted(el, binding) {
el._badge = `[${binding.value}]`
el.textContent = (el.textContent || '') + ' ' + el._badge
console.log(`[v-badge] mounted: badge = ${el._badge}`)
},
updated(el, binding) {
const oldBadge = `[${binding.oldValue}]`
const newBadge = `[${binding.value}]`
el.textContent = (el.textContent || '').replace(oldBadge, newBadge)
el._badge = newBadge
console.log(`[v-badge] updated: ${oldBadge} -> ${newBadge}`)
}
})
// --- Фейковый DOM-объект ---
const fakeEl = {
attributes: { 'data-v-highlight': '#3498db', 'data-v-badge': '5' },
style: {},
textContent: 'Кнопка',
hasAttribute(name) { return name in this.attributes },
getAttribute(name) { return this.attributes[name] ?? null },
getAttributeNames() { return Object.keys(this.attributes) },
setAttribute(name, val) { this.attributes[name] = val },
}
console.log('=== Применяем директивы ===')
system.applyToElement(fakeEl)
console.log('\n=== Обновляем директивы ===')
system.update(fakeEl, 'highlight', '#e74c3c')
system.update(fakeEl, 'badge', 12)
console.log('\n=== Размонтируем ===')
system.unmount(fakeEl, 'highlight')Создай Vue-приложение с кастомной директивой `v-highlight`. При монтировании элемента директива должна устанавливать фоновый цвет равный переданному значению. Зарегистрируй директиву глобально через `app.directive()`. Используй её на нескольких элементах с разными цветами. Добавь кнопку для смены цвета через реактивную переменную.
В mounted и updated используй binding.value для получения цвета. Первый элемент: v-highlight="'#bfdbfe'". Второй: v-highlight="dynamicColor" — значение берётся из ref. В updated обновляй el.style.backgroundColor = binding.value.