Vue.js: Diretivas Personalizadas

Última atualização: 2026-08-26

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


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:

VUE
<!-- ❌ The "Broken" Version:5 a button,5 Permission Code -->
<template>
  <button v-if="hasPermission('user.create')" @click="createUser">Create</button>
  <button v-if="hasPermission('user.delete')" @click="deleteUser">Delete</button>
  <button v-if="hasPermission('user.edit')" @click="editUser">Edit</button>
  <button v-if="hasPermission('order.create')" @click="createOrder">Create Order</button>
  <button v-if="hasPermission('order.cancel')" @click="cancelOrder">Cancel</button>
</template>

<script setup>
function hasPermission(perm) {
  return user.value.permissions?.includes(perm)
}
</script>

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

JS
// 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.
    }
  }
}
JS
// main.js
import { permission } from './directives/permission'
app.directive('permission', permission)
VUE
<!-- Usage: 1 v-permission replaces all v-if -->
<template>
  <button v-permission="'user.create'" @click="createUser">Create</button>
  <button v-permission="'user.delete'" @click="deleteUser">Delete</button>
  <button v-permission="'order.create'" @click="createOrder">Create Order</button>
</template>

50 botões, 50 v-permission — 50% mais conciso do que 50 v-if.

(3) Receita

Após as diretivas personalizadas:


3. Noções básicas sobre comandos personalizados

(1) Três maneiras de se inscrever

JS
// 1. Global Commands(main.js)
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// Global Registration:All components are available v-focus
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

app.mount('#app')
VUE
<!-- Local Instructions(Recommendations) -->
<!-- src/components/Input.vue -->
<script setup>
// Local Registration:Only this component works
const vFocus = {
  mounted(el) {
    el.focus()
  }
}
</script>

<template>
  <input v-focus>
</template>
JS
// 2. Abbreviation(mounted + updated)
app.directive('color', (el, binding) => {
  el.style.color = binding.value
})

(2) Os 5 principais pontos-chave do ciclo de vida

JS
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

JS
// 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

JS
// directives/focus.js
export const focus = {
  mounted(el, binding) {
    if (binding.value !== false) {
      el.focus()
    }
  }
}
VUE
<template>
  <!-- 1. Autofocus -->
  <input v-focus>
  
  <!-- 2. Focus on Conditions -->
  <input v-focus="shouldFocus">
  
  <!-- 3. Delayed Focus -->
  <input v-focus:delay="500">
</template>

(2) v-permission: Controle de permissões

JS
// 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)
      }
    }
  }
}
VUE
<template>
  <!-- Single Permission -->
  <button v-permission="'user.create'">Create</button>
  
  <!-- Multiple Permissions(All met)-->
  <button v-permission="['user.read', 'user.write']">Edit</button>
  
  <!-- Disable when permissions are lacking(rather than removing)-->
  <button v-permission.disable="'user.delete'">Delete</button>
</template>

(3) v-debounce: Supressão de rebotes de eventos

JS
// 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__)
    }
  }
}
VUE
<template>
  <button v-debounce="handleClick" v-debounce:500="handleClick">Click me</button>
  <input v-debounce="handleInput" v-debounce:1000="handleInput">
</template>

<script setup>
function handleClick() {
  console.log('Clicked (debounced 500ms)')
}
</script>

(4) v-copy: Clique para copiar

JS
// 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)
      }
    })
  }
}
VUE
<template>
  <button v-copy="shareUrl">Copy Link</button>
  <code v-copy="apiKey">Click to copy</code>
</template>

(5) v-lazy-load: Carregamento diferido de imagens

JS
// 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()
  }
}
VUE
<template>
  <img v-lazy-load="imageUrl" alt="...">
</template>

5. Parâmetros, modificadores e valores das instruções

(1) Três tipos de comandos

VUE
<!-- 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

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

VUE
<!-- 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>

6. Exemplo completo: 5 comandos principais + prática prática

▶ Exemplo: 1. Implementação completa do v-focus

JS
export const focus = {
  mounted(el, binding) {
    if (binding.value === false) return
    if (binding.arg) {
      setTimeout(() => el.focus(), parseInt(binding.arg))
    } else {
      el.focus()
    }
  }
}
▶ Experimente

▶ Exemplo: 2. Implementação completa do v-permission

JS
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)
  }
}
▶ Experimente

▶ Exemplo: 3. Implementação completa do v-debounce

JS
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)
  }
}
▶ Experimente

▶ Exemplo: 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

▶ Exemplo: 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

▶ Exemplo: 6. 5 dicas para usar comandos integrados

JS
// 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
    }
  }
})
▶ Experimente

❓ Perguntas Frequentes

P: Como faço para escolher entre diretivas personalizadas e componentes? R: Use diretivas para operações de baixo nível no DOM (como foco, rolagem e canvas). Use componentes para interfaces de usuário complexas (que envolvam estado, eventos e dados). As diretivas não possuem estado, enquanto os componentes possuem estado.

P: O value de uma diretiva é reativo? R: Alterações no 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.

P: Como se escreve v-permission:disable? R: arg='disable', modifiers.disable=true. A diretiva binding.modifiers.disable determina se ela deve ser desativada ou removida.

P: As diretivas podem usar TypeScript? R: Sim. O Vue 3.3+ oferece suporte a defineDirective e aos genéricos do TypeScript. No entanto, geralmente é mais simples usar apenas objetos JavaScript.

P: Como faço para escolher entre diretivas globais e locais? R: Use diretivas locais para recursos específicos do negócio (v-permission). Use diretivas globais para ferramentas de uso geral (v-focus / v-copy). Este tutorial recomenda o uso de diretivas locais (mais fáceis de manter).

P: Um comando pode passar uma referência? R: Sim. <input v-my-directive="myRef" />, em que binding.value no comando é o objeto de referência. No entanto, isso não é recomendado (antipadrão).

P: A biblioteca VueUse possui diretivas? R: Sim. 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


📝 Exercícios

  1. 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)
  2. 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)
  3. Problema desafiador (Dificuldade: ⭐⭐⭐)

    Implementar um “conjunto de instruções” completo:

    1. 5 diretivas: v-focus / v-permission / v-debounce / v-copy / v-lazy-load
    2. Implementação completa de cada instrução + tipos do TypeScript
    3. Exportar tudo do directives/index.js
    4. 5 casos de teste (usando o Vitest)
    5. Utilização em cenários reais (5 cenários diferentes no back-end de um site de comércio eletrônico)
    6. Documentar a API e os parâmetros de cada comando
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%