Vue.js: Diretivas Personalizadas
Última atualização: 2026-10-20
As diretivas personalizadas permitem que você amplie a sintaxe de modelos do Vue — usando atributos especiais que começam com v- para manipular diretamente o DOM subjacente. O Vue vem com diretivas integradas, como v-if, v-for e v-model; você pode criar suas próprias diretivas, como v-focus, v-permission e v-debounce.
As diretivas personalizadas são uma forma poderosa de criar “ferramentas DOM de baixo nível” — elas encapsulam operações DOM reutilizáveis em uma sintaxe declarativa. Compreender os cinco principais ganchos do ciclo de vida permite que você crie todos os tipos de diretivas v.
1. O que você vai aprender
- A essência dos comandos personalizados e seus três principais casos de uso
- Comando Global
app.directive() - Instrução local
directives: {} - 5 ganchos de ciclo de vida (created/beforeMount/mounted/beforeUpdate/unmounted)
- 5 diretrizes práticas (v-focus / v-permission / v-debounce / v-copy / v-lazy-load)
- Parâmetros, modificadores e valores de comando
- 5 antipadrões (uso indevido, sobrescrita de funções embutidas, esquecimento de limpar recursos, etc.)
2. O pesadelo de um botão de permissão “repetido em cinco lugares”
(1) Problema: Todos os 5 botões exigem verificações de permissão
O painel de administração da Alice tinha 5 botões que exigiam verificações de permissão:
5 botões × 5 verificações de permissão = 25 linhas de código repetitivo. Adicionar um novo botão exige escrever mais um v-if.
O gerente de produto Charlie:
“Alice, temos mais de 50 botões no painel de administração. Precisamos de uma diretiva ‘v-permission’ para deixar isso mais organizado.”
(2) Solução utilizando uma diretiva Vue personalizada: 1 v-permission
// directives/permission.js
export const permission = {
mounted(el, binding) {
const { value } = binding // 'user.create'
const userPermissions = getCurrentUser().permissions || []
if (!userPermissions.includes(value)) {
el.parentNode?.removeChild(el) // If you don't have permission, remove it.
}
}
}
// main.js
import { permission } from './directives/permission'
app.directive('permission', permission)
50 botões, 50 v-permission — 50% mais conciso do que 50 v-if.
(3) Receita
Após as diretivas personalizadas:
- Tamanho do código: 25 linhas de v-if → 3 linhas de v-permission (-88%)
- Novo botão: 1 v-permission substitui 1 v-if
- Lógica de permissões centralizada: 1 arquivo directives/permission.js
- Reutilizável: o v-permission também pode ser usado em outros projetos
3. Noções básicas sobre comandos personalizados
(1) Três maneiras de se inscrever
// 2. Abbreviation(mounted + updated)
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
(2) Os 5 principais pontos-chave do ciclo de vida
app.directive('demo', {
// 1. created(Command Creation)
created(el, binding) {
console.log('1. Command Creation')
},
// 2. beforeMount(Before mounting the element)
beforeMount(el) {
console.log('2. Before Mounting')
},
// 3. mounted(The element has been mounted)⭐ Most Commonly Used
mounted(el, binding) {
console.log('3. Mounted')
},
// 4. beforeUpdate(Before the dependency update)
beforeUpdate(el, binding) {
console.log('4. Before the update')
},
// 5. updated(After the dependency update)
updated(el, binding) {
console.log('5. Updated')
},
// 6. beforeUnmount(Before Uninstalling)
beforeUnmount(el) {
console.log('6. Before Uninstalling')
},
// 7. unmounted(After uninstallation)⭐ For cleaning
unmounted(el) {
console.log('7. Uninstalled')
}
})
(3) Explicação detalhada dos parâmetros do hook
// el, binding, vnode, prevVnode 4 parameter
mounted(el, binding, vnode, prevVnode) {
// el: Elements Bound to Commands
el.style.color = 'red'
// binding: Instruction Information Object
binding.value // Instruction Value, e.g. v-foo="bar" → bar
binding.arg // Parameters, e.g. v-foo:arg → 'arg'
binding.modifiers // Modifiers, e.g. v-foo.bar → { bar: true }
binding.instance // Component Instances That Use Commands
binding.dir // Instruction-Defined Objects
// vnode: Vue Virtual Node(Generally not used)
// prevVnode: Previous Virtual Node
}
4. 5 comandos essenciais para uso na prática
(1) v-focus: Foco automático
// directives/focus.js
export const focus = {
mounted(el, binding) {
if (binding.value !== false) {
el.focus()
}
}
}
(2) v-permission: Controle de permissões
// directives/permission.js
import { getCurrentUser } from '@/utils/auth'
export const permission = {
mounted(el, binding) {
const { value, modifiers } = binding
const user = getCurrentUser()
// value: String 'user.create' or Array ['user.create', 'user.delete']
// modifiers.disable: Disable, not remove
const required = Array.isArray(value) ? value : [value]
const hasPermission = required.every(p =>
user.permissions?.includes(p)
)
if (!hasPermission) {
if (modifiers.disable) {
el.disabled = true
el.title = 'No permission'
} else {
el.parentNode?.removeChild(el)
}
}
}
}
(3) v-debounce: Supressão de rebotes de eventos
// directives/debounce.js
export const debounce = {
mounted(el, binding) {
const { value, arg = 300 } = binding
if (typeof value !== 'function') {
console.warn('v-debounce: value must be a function')
return
}
let timer = null
el.__debounceTimer__ = timer
el.addEventListener('click', () => {
clearTimeout(timer)
timer = setTimeout(() => value(), arg)
el.__debounceTimer__ = timer
})
},
unmounted(el) {
if (el.__debounceTimer__) {
clearTimeout(el.__debounceTimer__)
}
}
}
(4) v-copy: Clique para copiar
// directives/copy.js
export const copy = {
mounted(el, binding) {
el.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(binding.value)
const original = el.textContent
el.textContent = 'Copied!'
setTimeout(() => { el.textContent = original }, 1500)
} catch (err) {
console.error('Copy failed:', err)
}
})
}
}
(5) v-lazy-load: Carregamento diferido de imagens
// directives/lazyLoad.js
export const lazyLoad = {
mounted(el, binding) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
observer.observe(el)
el.__observer__ = observer
},
unmounted(el) {
el.__observer__?.disconnect()
}
}
▶ Exemplo: Implementação completa do v-focus
export const focus = {
mounted(el, binding) {
if (binding.value === false) return
if (binding.arg) {
setTimeout(() => el.focus(), parseInt(binding.arg))
} else {
el.focus()
}
}
}
Saída:
<input v-focus> recebe o foco assim que entra no DOM.
Com v-focus:500 o foco é aplicado 500 ms depois (binding.arg).
Com v-focus="false" nada acontece (mounted retorna logo no início).
▶ Exemplo: Implementação completa do v-debounce
export const debounce = {
mounted(el, binding) {
const fn = binding.value
const delay = parseInt(binding.arg) || 300
if (typeof fn !== 'function') {
console.warn('[v-debounce] value must be a function')
return
}
let timer = null
el.addEventListener('click', () => {
clearTimeout(timer)
timer = setTimeout(() => fn(), delay)
})
el._debounceTimer = timer
},
unmounted(el) {
if (el._debounceTimer) clearTimeout(el._debounceTimer)
}
}
Saída:
Cinco cliques rápidos em <button v-debounce="salvar"> executam salvar() uma única vez.
O atraso é 300 ms por padrão ou o valor de v-debounce:500.
Se o valor não for uma função, o console avisa [v-debounce] value must be a function e nada é ligado.
(1) 📊 4. Referência rápida para 5 erros comuns
| Erro | Sintoma | Solução |
|---|---|---|
| Nome da diretiva sem “v-” | Não funciona | v-focus (não focus) |
| o valor não é uma função | Não executar | Verificar v-debounce="handler" |
| Limpar recursos não desmontados | Vazamentos de memória | Limpar temporizadores/observadores |
| Substituindo as diretivas integradas do Vue | Erro | Não se chama “v-if”, etc. |
| Alterações no valor do comando não estão sendo reconhecidas | Montado apenas uma vez | Usando o gancho updated |
(2) 📊 5. Comparação das 5 principais métricas de desempenho
| Implementação | Reutilização | Desempenho | Aplicabilidade |
|---|---|---|---|
v-if + Funções utilitárias |
❌ | ⭐⭐⭐ | Única vez |
| Comandos personalizados | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Ferramentas DOM |
| Componentes globais | ⭐⭐⭐ | ⭐⭐⭐ | Interface de usuário complexa |
| Composable | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Lógica de negócios |
| Pinia | ⭐⭐⭐⭐ | ⭐⭐⭐ | Situação global |
5. Parâmetros, modificadores e valores das instruções
(1) Três tipos de comandos
<!-- 1. v-directive="value" (value) -->
<input v-foo="username">
<!-- 2. v-directive:arg(Parameters,Fixed String) -->
<input v-foo:delay="500">
<!-- 3. v-directive.modifier(Modifiers,Boolean objects) -->
<input v-foo.bar>
<!-- 4. Combination -->
<input v-foo:delay.bar="500">
(2) Métodos de recepção do JS
app.directive('demo', (el, binding) => {
// v-demo="123"
binding.value // 123
// v-demo:abc
binding.arg // 'abc'
// v-demo.foo
binding.modifiers // { foo: true }
// v-demo:abc.foo="123"
binding.value // 123
binding.arg // 'abc'
binding.modifiers // { foo: true }
})
(3) 5 principais cenários de combinação
<!-- Scene 1:v-permission:disable -->
<button v-permission:disable="'user.create'">
<!-- arg='disable', value='user.create' -->
</button>
<!-- Scene 2:v-debounce:500 -->
<button v-debounce:500="handler">
<!-- arg='500'(500ms Image Stabilization) -->
</button>
<!-- Scene 3:v-once.lazy -->
<img v-once.lazy="imageUrl">
<!-- modifiers.lazy=true, value=imageUrl -->
</template>
▶ Exemplo: Implementação completa do v-permission
import { getCurrentUser } from '@/utils/auth'
export const permission = {
mounted(el, binding) {
const { value, modifiers } = binding
const user = getCurrentUser()
const required = Array.isArray(value) ? value : [value]
const ok = required.every(p => user.permissions?.includes(p))
if (!ok) {
if (modifiers.disable) {
el.disabled = true
el.style.opacity = '0.5'
el.title = 'No permission'
} else {
el.parentNode?.removeChild(el)
}
}
},
updated(el, binding) {
// Permissions may change(User Role Switching),Re-examine
this.mounted(el, binding)
}
}
Saída:
Usuário sem a permissão exigida: o botão é removido do DOM.
Com o modificador .disable o botão fica acinzentado (opacity 0.5) e com title No permission.
Ao trocar de papel o hook updated roda de novo e libera ou bloqueia o elemento.
6. Exercício abrangente: recriando diretivas internas do Vue
▶ Exemplo: 5 dicas para usar comandos integrados
// Draw on Vue Implementation of Built-in Instructions
import { createApp } from 'vue'
const app = createApp({})
// 1. v-show(Control display)
app.directive('show', {
mounted(el, binding) { el.style.display = binding.value ? '' : 'none' },
updated(el, binding) { el.style.display = binding.value ? '' : 'none' }
})
// 2. v-text(Settings textContent)
app.directive('text', {
mounted(el, binding) { el.textContent = binding.value },
updated(el, binding) { el.textContent = binding.value }
})
// 3. v-html(Settings innerHTML,XSS Risks)
app.directive('html', {
mounted(el, binding) { el.innerHTML = binding.value },
updated(el, binding) { el.innerHTML = binding.value }
})
// 4. v-once(Render only once)
app.directive('once', {
mounted(el, binding, vnode) {
if (binding.value !== undefined) {
vnode.context[binding.arg] = binding.value
}
}
})
Saída:
v-show alterna display entre none e vazio conforme o valor.
v-text troca o textContent e v-html injeta o innerHTML (com risco de XSS).
v-once grava o valor no contexto apenas no mounted e ignora mudanças posteriores.
❓ Perguntas Frequentes
value de uma diretiva é reativo?value acionam o gancho updated. Para monitorar alterações no value, use watch(binding.value) ou compare os valores antigo e novo dentro do gancho updated.v-permission:disable?arg='disable', modifiers.disable=true. A diretiva binding.modifiers.disable determina se ela deve ser desativada ou removida.defineDirective e aos genéricos do TypeScript. No entanto, geralmente é mais simples usar apenas objetos JavaScript.<input v-my-directive="myRef" />, em que binding.value no comando é o objeto de referência. No entanto, isso não é recomendado (antipadrão).vFocus / vClickOutside / vLazyLoad / vInfiniteScroll e outras são todas diretivas fornecidas pela VueUse. É melhor usar a VueUse do que escrever suas próprias diretivas.📖 Resumo
- As diretivas personalizadas são atributos especiais que começam com “v-” e são usados para manipular diretamente o DOM
- 3 tipos de registro: global (app.directive) / local (directives: {}) / abreviado (montado e atualizado combinados)
- 5 ganchos do ciclo de vida: created / beforeMount / mounted / beforeUpdate / updated / beforeUnmount / unmounted
- 5 Prático: v-focus / v-permission / v-debounce / v-copy / v-lazy-load
- 3 formas: valor / argumento (arg) / modificador (modificadores)
- 5 anti-padrões: Esquecer o prefixo
v-/valuesem função / Esquecer de limpar / Substituir valores embutidos / Não responder às alterações emvalue
📝 Exercícios
-
Questões básicas (Dificuldade: ⭐)
Implementação da diretiva
v-focus:- Aceita um parâmetro
arg(atraso em milissegundos) - Aceita modificadores (prevent: impede o comportamento padrão)
- Aceita um parâmetro
-
Problemas avançados (Dificuldade: ⭐⭐)
Implementação da versão completa do v-permission:
- Suporta strings com permissão única: v-permission="'user.create'"
- Suporta matrizes de permissões: v-permission="['user.read', 'user.write']"
- Modificador compatível: .disable (desativa, em vez de remover)
- Com currentUser (fornecer/injetar Injection)
-
Problema desafiador (Dificuldade: ⭐⭐⭐)
Implementar um “conjunto de instruções” completo:
- 5 diretivas: v-focus / v-permission / v-debounce / v-copy / v-lazy-load
- Implementação completa de cada instrução + tipos do TypeScript
- Exportar tudo do
directives/index.js - 5 casos de teste (usando o Vitest)
- Utilização em cenários reais (5 cenários diferentes no back-end de um site de comércio eletrônico)
- Documentar a API e os parâmetros de cada comando