Vue.js: Comunicação entre Componentes
Última atualização: 2026-08-26
A comunicação entre componentes é um mecanismo central do Vue — os componentes pais passam dados para os componentes filhos por meio de props, e os componentes filhos acionam eventos usando emits para notificar os componentes pais. Compreender o fluxo bidirecional de dados é fundamental para criar boas aplicações Vue.
O <script setup> do Vue 3 oferece duas macros de tempo de compilação, defineProps e defineEmits, que podem ser usadas sem importação e oferecem suporte total à inferência de tipos do TypeScript.
1. O que você vai aprender
- Sintaxe completa para props: tipos, campos obrigatórios, valores padrão, validadores
- Várias formas de declarar props (em tempo de execução / por tipo / simplificada)
- Emite declarações e validações de eventos
- Inferência de tipos no TypeScript (
defineProps<T>()) - Passagem de propriedades (inheritAttrs / useAttrs)
- O princípio do fluxo unidirecional de dados
- 5 erros comuns e melhores práticas
2. A armadilha da “ligação bidirecional” no carrinho de compras de um site de comércio eletrônico
(1) Problema: Quando um componente filho altera uma propriedade, o componente pai gera um erro
Alice implementou um carrinho de compras com um campo para inserção da quantidade:
<!-- CartItem.vue Child component -->
<template>
<input :value="quantity" @input="quantity = $event.target.value">
</template>
<script setup>
// ❌ The "Broken" Version:Edit directly prop
const props = defineProps({ quantity: Number })
function update() {
props.quantity++ // ❌ Vue Warning:Avoid making direct changes prop
}
</script>
O Vue gera um erro:
[Vue warn] Set operation on key "quantity" failed: target is readonly.
Após uma hora de depuração, Charlie descobriu que:
“Alice, o total do carrinho do pai não é atualizado quando altero a quantidade. O Vue está alterando a propriedade, mas o pai não percebe.”
(2) Fluxo de dados unidirecional no Vue + Solução emit
<!-- CartItem.vue Child component -->
<template>
<div>
<input :value="quantity" @input="$emit('update:quantity', +$event.target.value)">
</div>
</template>
<script setup>
const props = defineProps({ quantity: Number })
const emit = defineEmits(['update:quantity'])
</script>
<!-- Cart.vue Parent Component -->
<template>
<CartItem
v-for="item in items"
:key="item.id"
:quantity="item.quantity"
@update:quantity="(val) => item.quantity = val"
/>
</template>
Agora as alterações fluem corretamente: o filho emite → o pai recebe → o pai atualiza os dados → repassa ao filho.
(3) Receita
Após corrigir o fluxo de dados:
- Aviso sobre visualizações: 100/dia → 0
- Consistência dos dados: Sincronização em tempo real com o componente pai
- Facilidade de depuração: O fluxo de dados é unidirecional, o que facilita o rastreamento
- Testabilidade: Os componentes podem ser testados de forma independente quanto a props e emit
3. Sintaxe completa para props
(1) Declarações para 7 tipos de adereços
<script setup>
defineProps({
// 1. Basic Types
name: String,
age: Number,
active: Boolean,
// 2. Complex Types
user: Object,
items: Array,
// 3. Multiple Types(Any)
id: [String, Number],
// 4. Required
title: { type: String, required: true },
// 5. Default value
pageSize: { type: Number, default: 20 },
// 6. Custom Validation
email: {
type: String,
validator: (val) => val.includes('@')
},
// 7. Function Default Values(The default value must be a function.)|
createdAt: {
type: Date,
default: () => new Date()
}
})
</script>
(2) Exemplo detalhado
<script setup>
defineProps({
// String
タイトル: String,
// Numbers + Default value + Verification
pageSize: {
type: Number,
default: 20,
validator: (val) => val > 0 && val <= 100
},
// Boolean(Note:The default value for falsy Values should be in function form)
isVip: {
type: Boolean,
default: false
},
// Array Default Values(It must be returned as a function)
tags: {
type: Array,
default: () => ['vue', 'javascript']
},
// Object Default Values(It must be returned as a function)
user: {
type: Object,
default: () => ({ name: 'Guest', age: 0 })
},
// Required + Custom Validation
email: {
type: String,
required: true,
validator: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
}
})
</script>
(3) 6 Regras de valor padrão
| Regra | Exemplo |
|---|---|
| Tipos básicos | default: 20 |
| Matrizes/Objetos | default: () => [] |
| null | default: null |
| indefinido | Não é necessário definir um valor padrão |
| Referência de funções | Não use default: fn; use () => fn() |
| Símbolo / BigInt | A função de fábrica retorna |
4. Props com tipagem forte no TypeScript (recomendado para o Vue 3)
(1) Declarações de tipos primitivos
<script setup lang="ts">
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
defineProps<{
user: User
size?: 'small' | 'medium' | 'large'
showEmail?: boolean
}>()
</script>
(2) Obrigatório x Opcional (?)
defineProps<{
// Required(Default)
user: User
pageSize: number
// Optional (add ?)
variant?: 'primary' | 'secondary'
showIcon?: boolean
}>()
(3) withDefaults fornece valores padrão
<script setup lang="ts">
interface Props {
user: User
size?: 'small' | 'medium' | 'large'
showEmail?: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
showEmail: true
})
</script>
(4) Comparação entre declarações em tempo de execução e declarações de tipo
| Dimensão | Tempo de execução defineProps({}) |
Tipo defineProps<T>() |
|---|---|---|
| Inferência de tipos | Fraca | Forte (autocompletar do IDE) |
| Valor padrão | Padrão direto | wrapper withDefaults |
| Validação | função de validação | verificação em tempo de compilação do TypeScript |
| Recomendado | Projetos pequenos/JS puro | Projetos grandes/projetos em TS |
5. Sintaxe completa para emit
(1) 5 tipos de declarações de evento
<script setup>
// 1. A Brief Statement
const emit = defineEmits(['click', 'submit', 'cancel'])
// 2. Parameter Validation
const emit = defineEmits({
addToCart: (productId) => typeof productId === 'number',
remove: (id) => typeof id === 'string'
})
// 3. TypeScript Type
const emit = defineEmits<{
'add-to-cart': [productId: number]
'remove-item': [id: string, reason: string]
'update': [id: number, data: object]
}>()
</script>
(2) 5 métodos de acionamento
<script setup>
const emit = defineEmits(['click', 'submit', 'add-to-cart'])
// 1. Simple Trigger
emit('click')
// 2. Passing Parameters
emit('add-to-cart', 123)
// 3. Pass multiple parameters
emit('submit', { name: 'Alice' }, 2026)
// 4. Conditional Trigger
if (isValid) emit('submit')
// 5. Functional Triggering
function handleAdd() {
emit('add-to-cart', props.product.id)
}
</script>
(3) Escuta do componente pai
<template>
<ChildComponent
@click="handleClick"
@add-to-cart="handleAdd"
@submit="handleSubmit"
/>
</template>
<script setup>
function handleClick() { /* No parameters */ }
function handleAdd(productId) { /* 1 parameter */ }
function handleSubmit(data, year) { /* Multiple parameters */ }
</script>
(4) Validação de tipo para emit
<script setup>
const emit = defineEmits({
// ✅ Verification:id It must be number
remove: (id) => typeof id === 'number' || 'Validation failed',
// ❌ Throws Error: When parameters do not match
// remove: 'invalid', // Wrong: It must be a function
})
</script>
6. O princípio do fluxo unidirecional de dados
(1) Princípios Fundamentais
graph LR
A[Parent Component data] -->|props| B[Child component]
B -->|emit| A
style A fill:#42b883
style B fill:#42b883
- Os dados só podem fluir do pai para o filho (as propriedades fluem de cima para baixo)
- Os eventos só podem fluir do filho para o pai (emissão ascendente)
- Os componentes filhos não podem modificar diretamente as propriedades
(2) 5 Exceções
<script setup>
const props = defineProps({ user: Object })
// ❌ Wrong: Edit prop directly
props.user.name = 'Bob' // Vue Warning!
// ✅ Exception 1:prop When it is a reference type,Can be replaced with a new object
// The parent component passes a reference,Child components cannot change their references,But it's possible emit Let Father Change It
// ✅ Exception 2:prop Initial values can be saved locally
const localUser = ref({ ...props.user })
// Then make the changes localUser(Does not affect the parent)
// ✅ Exception 3: Use computed Derivative
const userName = computed(() => props.user.name)
// ✅ Exception 4: Use v-model (Syntax Sugar)
// <Child v-model="value" /> equivalent to :value + @update:value
// ✅ Exception 5:provide/inject Across levels
// Parent provide, Child inject (Learned in Phase 2.5)
</script>
(3) 5 antipadrões
// ❌ Anti-pattern 1:Edit directly prop
props.user.name = 'Bob'
// ❌ Anti-pattern 2:Use in child components watch Edit prop
watch(() => props.value, (val) => { props.value = val * 2 })
// ❌ Anti-pattern 3: Two-way bound ref passthrough (Not recommended)
const localRef = ref(props.value)
watch(localRef, (val) => emit('update', val))
// ❌ Anti-pattern 4:Usage v-model Does not follow the naming convention
// v-model Expectations update:xxx Event,Custom event names must be consistent
emit('change', val) // ❌ Parent Component @change Will not be triggered
emit('update:value', val) // ✅ v-model Supporting
// ❌ Anti-pattern 5:Managing the parent component's data directly within a child component
emit('update', { ...props.user, name: 'Bob' }) // Wrong: Should only pass ID, Parent updates itselff
7. Passagem de propriedades: inheritAttrs e useAttrs
(1) Comportamento padrão
<!-- Parent.vue -->
<template>
<ChildComponent class="parent-class" :title="title" />
</template>
<!-- ChildComponent.vue - By default, messages are received automatically. class="parent-class" -->
<template>
<div> <!-- Automatically available class="parent-class" -->
<h3>{{ title }}</h3>
</div>
</template>
(2) Desativar o repasse automático
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<!-- class It will not be automatically applied to the root element -->
<div class="my-class">
<h3>Title</h3>
</div>
</template>
(3) Uso explícito de useAttrs
<script setup>
import { useAttrs } from 'vue'
defineOptions({ inheritAttrs: false })
const attrs = useAttrs()
</script>
<template>
<div>
<input v-bind="attrs" />
<!-- Pass the value from the parent component class/style/Manually bind other properties to input -->
</div>
</template>
(4) 5 Casos de uso
| Cenário | Método |
|---|---|
| Padrão | Não é necessária nenhuma ação; a classe é aplicada automaticamente à raiz |
| Elemento raiz personalizado | useAttrs() Vinculação explícita |
| Propagar para elementos que não sejam raiz | inheritAttrs: false + v-bind="$attrs" |
| Vários componentes (Fragmentos) | É necessário usar inheritAttrs: false + ligação explícita |
| Otimização de desempenho | Desativar o repasse para evitar atributos DOM desnecessários |
8. Exemplo completo: Props/emits do cartão de produto no comércio eletrônico
▶ Exemplo: 1. ProductCard.vue — Props e emits completos
<!-- src/components/ProductCard.vue -->
<template>
<div :class="['product-card', { 'out-of-stock': !inStock }]">
<img :src="product.image" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<span v-if="lowStock" class="badge">Only {{ product.stock }} left</span>
<button :disabled="!inStock" @click="handleAddToCart">
{{ inStock ? 'Add to Cart' : 'Out of Stock' }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Product {
id: number
name: string
price: number
stock: number
image: string
}
// props Complete Definition
const props = defineProps({
product: {
type: Object as () => Product,
required: true,
validator: (val: Product) => val.id && val.name
},
showStockBadge: { type: Boolean, default: true }
})
// emits Complete Definition
const emit = defineEmits({
'add-to-cart': (productId: number) => typeof productId === 'number',
'quick-view': (productId: number) => typeof productId === 'number',
'toggle-favorite': (productId: number, isFavorite: boolean) =>
typeof productId === 'number' && typeof isFavorite === 'boolean'
})
// Computed Properties
const inStock = computed(() => props.product.stock > 0)
const lowStock = computed(() => props.product.stock > 0 && props.product.stock < 10)
// Event Handling
function handleAddToCart() {
if (inStock.value) {
emit('add-to-cart', props.product.id)
}
}
</script>
<style scoped>
.product-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
transition: all 0.2s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.out-of-stock { opacity: 0.6; }
.badge { background: #f59e0b; color: white; padding: 2px 8px; border-radius: 4px; }
.price { color: #42b883; font-weight: bold; }
</style>
▶ Exemplo: 2. Componente pai utilizando ProductCard
<!-- src/views/ProductListView.vue -->
<template>
<div class="grid">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
:show-stock-badge="true"
@add-to-cart="handleAddToCart"
@quick-view="handleQuickView"
@toggle-favorite="handleToggleFavorite"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ProductCard from '@/components/ProductCard.vue'
import { useCart } from '@/composables/useCart'
const products = ref([
{ id: 1, name: 'iPhone', price: 999, stock: 50, image: 'iphone.jpg' },
{ id: 2, name: 'MacBook', price: 2499, stock: 0, image: 'macbook.jpg' }
])
const { addToCart } = useCart()
function handleAddToCart(productId) {
addToCart(productId)
}
function handleQuickView(productId) {
console.log('Quick view:', productId)
}
function handleToggleFavorite(productId, isFavorite) {
console.log('Toggle:', productId, isFavorite)
}
</script>
▶ Exemplo: 3. Vinculação bidirecional com v-model
<!-- Child component CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- Using Parent Components v-model -->
<template>
<CustomInput v-model="searchQuery" />
<p>Query: {{ searchQuery }}</p>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from '@/components/CustomInput.vue'
const searchQuery = ref('')
</script>
▶ Exemplo: 4. useAttrs Pass-Through
<!-- CustomInput.vue - Multi-element components -->
<template>
<!-- Multiple elements must be used with inheritAttrs: false -->
<label>{{ label }}</label>
<input v-bind="$attrs" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
defineOptions({ inheritAttrs: false })
defineProps({
modelValue: String,
label: String
})
defineEmits(['update:modelValue'])
</script>
<!-- Parent Component -->
<CustomInput v-model="name" label="Name" placeholder="Enter name..." class="input-field" />
<!-- Placeholder and class will be auto-applied to input (No label) -->
▶ Exemplo: 5. Referência rápida para 5 erros comuns
| Erro | Sintoma | Solução |
|---|---|---|
| Modificar a propriedade diretamente | Aviso do Vue | Use emit para permitir que o pai a modifique |
| Matrizes/objetos: as propriedades têm valores padrão como referências | Compartilhadas por vários componentes | default: () => [] Fábrica |
| Incompatibilidade no nome do evento do v-model | O pai não o recebe | Use update:xxx |
| o validador retorna uma string | o Vue não gera um erro | retorna true/false |
| Repasse para vários componentes | Aviso do Vue | inheritAttrs: false |
▶ Exemplo: 6. Cinco principais cenários de validação para props
| Cenário | Verificação |
|---|---|
val => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) |
|
| URL | val => /^https?:\/\//.test(val) |
| Valor da enumeração | val => ['a', 'b', 'c'].includes(val) |
| Gama | val => val >= 0 && val <= 100 |
| Objeto personalizado | val => val.id && val.name |
❓ Perguntas Frequentes
P: É possível modificar props diretamente? R: Não. De acordo com o princípio do fluxo de dados unidirecional do Vue, os props são somente de leitura. Para modificar os dados, um componente filho deve emitir um evento para que o componente pai possa realizar a alteração. Modificar props diretamente acionará um aviso do Vue.
P: Como faço para passar dados complexos (objetos aninhados)? R: Passe a referência diretamente; o Vue lida automaticamente com o binding profundo. Quando você modifica uma propriedade dentro do objeto, o componente pai detecta a alteração (binding profundo), mas os componentes filhos não devem modificá-la diretamente; em vez disso, devem usar
emitpara permitir que o componente pai atualize o objeto ou a propriedade inteira.
P: Qual é a diferença entre as opções
definePropseprops? R: No<script setup>, use a macrodefineProps()(não é necessária importação); na API de Opções, use a opçãoprops: {}. As duas são equivalentes; recomendamos usarsetup+defineProps.
P: O
emitpode ser acionado de forma assíncrona em um componente filho? R: Sim. Oemité uma chamada de função síncrona, mas pode ser encapsulado emsetTimeoutou em umPromise:setTimeout(() => emit('done'), 1000).
P: Qual é a essência de
v-model? R: Trata-se de um sintaxe simplificada parapropseemit.<Child v-model="x" />é equivalente a:modelValue="x" @update:modelValue="x = $event". Consulte o Exemplo 3 desta lição para obter uma explicação detalhada.
P: Como faço para monitorar alterações nas propriedades de um componente filho? R: Use
watch(() => props.xxx, (newVal, oldVal) => {...}). Tome cuidado para não modificar as propriedades dentro do watch, ou você entrará em um loop infinito.
P: Quando se deve usar
useAttrs? R: Em três situações: (1) É necessário para componentes com múltiplas raízes (Fragments); (2) Quando se deseja passar atributos para elementos que não são raiz; (3) Quando se deseja controlar explicitamente onde os atributos são vinculados. Não é necessário para componentes comuns com uma única raiz.
P: O que acontece se a validação dos props falhar? R: No ambiente de desenvolvimento, o Vue exibe um aviso; no ambiente de produção, a validação falha silenciosamente. Recomenda-se que o validador retorne
false(erro) ou uma string (mensagem de erro).
📖 Resumo
- props: Use
defineProps()ao passar do pai para o filho edefineEmits()ao passar do filho para o pai - 7 tipos de props: Básicos / Complexos / Multitipo / Obrigatórios / Padrão / De validação / De fábrica
- O TypeScript utiliza
defineProps<T>()para tipagem forte +withDefaults()com valores padrão - 5 tipos de gatilhos + validação de tipo
- Princípio do fluxo unidirecional de dados: os dados fluem de baixo para cima, os eventos fluem de cima para baixo
- 5 anti-padrões: Modificação direta de props / Modificação de props via
watch/ Vazamento de dados de referência bidirecional / Incompatibilidade de nomes de eventos no modelo V / Filho gerenciando dados do pai - inheritAttrs / useAttrs para o repasse de propriedades
v-modelé uma sintaxe simplificada parapropseemit
📝 Exercícios
-
Questões básicas (Dificuldade: ⭐)
Implemente um componente Button simples:
- parâmetros:
text(String),variant(String, 'primário'/'sucesso'/'perigo'),disabled(Booleano) - As 3 variantes correspondem a 3 classes CSS
- emitir o evento
click(sem parâmetros) - Teste as três variantes no componente pai
- parâmetros:
-
Problemas avançados (Dificuldade: ⭐⭐)
Implemente um componente de entrada com vinculação bidirecional:
- Child CustomInput.vue: props:
modelValue,label - Usando o “sugar sintático”
v-model - emitir
update:modelValue - Componente pai: exibe as entradas do usuário em tempo real
- Child CustomInput.vue: props:
-
Problema de desafio (Dificuldade: ⭐⭐⭐)
Implementar um sistema abrangente de “Ficha do produto + Favoritos”:
- ProductCard.vue: Props completas (product/showStockBadge/maxStock) + emissões (add-to-cart/toggle-favorite)
- App.vue: Lista de produtos + Status dos favoritos
- Botão “Favorito”: emite o evento
toggle-favorite; o componente pai atualiza o status de favorito - Implementar componentes com múltiplas raízes usando
useAttrseinheritAttrs(sem comprometer o exercício) - Verificado por pelo menos 3 validadores (e-mail/URL/enum)
- Props fortemente tipadas do TypeScript