Vue3 组件通信实战 | 8 种组件通信方式全解析

零、为什么组件通信是 Vue3 的核心难点?

想象一下这个场景:你正在开发一个复杂的后台管理系统,有一个需求是在 5 层嵌套的组件中,需要把最底层的表单数据提交到最顶层的页面 。这时候你怎么办?一层层 props 传上去?还是用 $emit 冒泡?

更头疼的是:面试必问!

"Vue3 中有哪些组件通信方式?分别适用于什么场景?"

今天,我们就用一篇文章,把 Vue3 的 8 种组件通信方式讲透,附代码 + 场景 + 选型建议。

一、组件关系图谱

先搞清楚组件之间的关系,不同关系选择不同的通信方式:

text 复制代码
┌──────────────────────────────────────────┐
│            App (根组件)                 │
│  ┌─────────────────────────────────────┐ │
│  │         Parent (父组件)            │ │
│  │  ┌───────────┐  ┌───────────┐       │ │
│  │  │  Child    │  │  Child    │       │ │
│  │  │ ┌───────┐ │  └───────────┘       │ │
│  │  │ │Grand- │ │                      │ │
│  │  │ │Child  │ │                      │ │
│  │  │ └───────┘ │                      │ │
│  │  └───────────┘                      │ │
│  └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘

通信关系:
- 父子:props / emit / ref / v-model
- 祖孙:provide / inject
- 兄弟:mitt / eventBus
- 全局:Pinia / Vuex

二、父子组件通信(4 种方式)

2.1 Props / Emit --- 最基础的通信

Parent.vue

vue 复制代码
<!-- Parent.vue -->
<template>
  <div class="parent">
    <h2>父组件</h2>
    <p>收到子组件消息:{{ childMessage }}</p>
    
    <Child 
      :message="parentMessage" 
      @reply="handleReply"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

// 传递给子组件的数据
const parentMessage = ref('我是父组件的数据')

// 接收子组件的消息
const childMessage = ref('')

const handleReply = (msg: string) => {
  childMessage.value = msg
}
</script>

Child.vue

vue 复制代码
<!-- Child.vue -->
<template>
  <div class="child">
    <h3>子组件</h3>
    <p>收到父组件消息:{{ message }}</p>
    <button @click="sendReply">回复父组件</button>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

// 接收 props
const props = defineProps<{
  message: string
}>()

// 定义 emit
const emit = defineEmits<{
  (e: 'reply', msg: string): void
}>()

const sendReply = () => {
  emit('reply', '你好,父组件!我是子组件')
}
</script>

TypeScript 类型提示 :Vue3 的 definePropsdefineEmits 提供了完整的类型推导。

2.2 v-model --- 双向绑定的优雅写法

Parent.vue

vue 复制代码
<!-- Parent.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <p>用户名:{{ username }}</p>
    
    <!-- v-model 语法糖 -->
    <Child v-model="username" />
    
    <!-- 等价于 -->
    <!-- <Child :modelValue="username" @update:modelValue="username = $event" /> -->
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const username = ref('张三')
</script>

Child.vue

vue 复制代码
<!-- Child.vue -->
<template>
  <div>
    <h3>子组件 - 编辑用户名</h3>
    <input 
      :value="modelValue" 
      @input="updateValue"
      placeholder="输入用户名"
    />
  </div>
</template>

<script setup lang="ts">
defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: string): void
}>()

const updateValue = (e: Event) => {
  const target = e.target as HTMLInputElement
  emit('update:modelValue', target.value)
}
</script>

多个 v-model:Vue3 支持绑定多个值

vue 复制代码
<template>
  <Child v-model:name="name" v-model:age="age" />
</template>

<!-- Child.vue -->
<script setup>
defineProps<{
  name: string
  age: number
}>()

const emit = defineEmits<{
  (e: 'update:name', value: string): void
  (e: 'update:age', value: number): void
}>()
</script>

2.3 ref / defineExpose --- 父组件调用子组件方法

Parent.vue

vue 复制代码
<!-- Parent.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="callChildMethod">调用子组件方法</button>
    <button @click="getChildData">获取子组件数据</button>
    
    <Child ref="childRef" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

// 获取子组件实例
const childRef = ref<InstanceType<typeof Child>>()

const callChildMethod = () => {
  // 调用子组件暴露的方法
  childRef.value?.sayHello()
}

const getChildData = () => {
  // 获取子组件暴露的数据
  console.log('子组件内部计数:', childRef.value?.count)
  alert(`子组件计数: ${childRef.value?.count}`)
}
</script>

Child.vue

vue 复制代码
<!-- Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <p>内部计数: {{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)

const sayHello = () => {
  alert('你好,我是子组件!')
}

// 暴露给父组件的方法和数据
defineExpose({
  count,
  sayHello
})
</script>

2.4 透传 Attributes --- $attrs 的妙用

Parent.vue

vue 复制代码
<!-- Parent.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <Child 
      class="custom-class"
      data-id="123"
      @click="handleClick"
      :custom-prop="'hello'"
    />
  </div>
</template>

<script setup>
const handleClick = () => {
  console.log('点击事件')
}
</script>

Child.vue

vue 复制代码
<!-- Child.vue -->
<template>
  <div>
    <h3>子组件</h3>
    <!-- 透传所有属性到内部元素 -->
    <button v-bind="$attrs">透传按钮</button>
  </div>
</template>

<script setup lang="ts">
import { useAttrs } from 'vue'

// 获取透传属性(不包含 props 和 emit)
const attrs = useAttrs()
console.log('透传属性:', attrs)
// 输出: { class: 'custom-class', 'data-id': '123', onClick: fn, customProp: 'hello' }
</script>

三、祖孙组件通信(2 种方式)

3.1 provide / inject --- 跨层级传递数据

GrandParent.vue

vue 复制代码
<!-- GrandParent.vue -->
<template>
  <div>
    <h2>祖组件</h2>
    <p>主题色: {{ themeColor }}</p>
    <button @click="changeTheme">切换主题</button>
    
    <Parent />
  </div>
</template>

<script setup lang="ts">
import { ref, provide } from 'vue'
import Parent from './Parent.vue'

const themeColor = ref('#409eff')

// 提供数据给所有后代组件
provide('themeColor', themeColor)

// 提供方法
const changeTheme = () => {
  themeColor.value = themeColor.value === '#409eff' ? '#67c23a' : '#409eff'
}

provide('changeTheme', changeTheme)
</script>

Parent.vue

vue 复制代码
<!-- Parent.vue -->
<template>
  <div>
    <h3>父组件(中间层)</h3>
    <Child />
  </div>
</template>

<script setup>
import Child from './Child.vue'
// 注意:中间组件不需要做任何事
</script>

Child.vue

vue 复制代码
<!-- Child.vue -->
<template>
  <div>
    <h4>孙组件</h4>
    <p>当前主题色: <span :style="{ color: themeColor }">{{ themeColor }}</span></p>
    <button @click="changeTheme">改变主题(调用祖先方法)</button>
  </div>
</template>

<script setup lang="ts">
import { inject } from 'vue'

// 注入祖先组件提供的数据
const themeColor = inject<Ref<string>>('themeColor', ref('#409eff'))

// 注入祖先组件提供的方法
const changeTheme = inject<() => void>('changeTheme', () => {})

// 也可以注入响应式数据直接修改(但不推荐,单向数据流)
// const updateTheme = inject<(color: string) => void>('updateTheme')
</script>

响应式 provide :使用 refreactive 实现响应式传递

typescript 复制代码
// 推荐:提供响应式数据
const state = reactive({
  user: null,
  theme: 'light'
})
provide('appState', state)

// 后代组件中可以修改(需要约定)
const appState = inject('appState')
appState.user = { name: '张三' } // 会影响祖先

3.2 插槽 (Slot) --- 模板分发的高级用法

GrandParent.vue

vue 复制代码
<!-- GrandParent.vue -->
<template>
  <div>
    <h2>祖组件</h2>
    <Parent>
      <!-- 作用域插槽:将数据传递给父组件 -->
      <template #default="{ user }">
        <div class="user-card">
          <h3>{{ user.name }}</h3>
          <p>{{ user.email }}</p>
        </div>
      </template>
    </Parent>
  </div>
</template>

<script setup>
import Parent from './Parent.vue'

// 数据在祖组件中
const users = [
  { id: 1, name: '张三', email: 'zhangsan@example.com' },
  { id: 2, name: '李四', email: 'lisi@example.com' }
]
</script>

Parent.vue

vue 复制代码
<!-- Parent.vue -->
<template>
  <div>
    <h3>父组件(传递数据)</h3>
    <Child>
      <!-- 将数据通过插槽传递给上级 -->
      <template #default="{ user }">
        <slot :user="user" />
      </template>
    </Child>
  </div>
</template>

<script setup>
import Child from './Child.vue'
</script>

Child.vue

xml 复制代码
<!-- Child.vue -->
<template>
  <div>
    <h4>孙组件(渲染数据)</h4>
    <div v-for="user in users" :key="user.id">
      <slot :user="user" />
    </div>
  </div>
</template>

<script setup>
const users = [
  { id: 1, name: '张三', email: 'zhangsan@example.com' },
  { id: 2, name: '李四', email: 'lisi@example.com' }
]
</script>

四、兄弟组件通信(1 种方式)

mitt --- 轻量级事件总线

bash 复制代码
npm install mitt

utils/eventBus.ts

typescript 复制代码
// utils/eventBus.ts
import mitt from 'mitt'
import type { Emitter } from 'mitt'

// 定义事件类型
type Events = {
  'user:login': { username: string; token: string }
  'user:logout': void
  'cart:update': number  // 购物车数量
  'notification': string
}

const emitter: Emitter<Events> = mitt<Events>()
export default emitter

ComponentA.vue (发送方)

vue 复制代码
<!-- ComponentA.vue (发送方) -->
<template>
  <div>
    <h3>组件 A</h3>
    <button @click="handleLogin">登录</button>
    <button @click="addToCart">添加购物车</button>
  </div>
</template>

<script setup lang="ts">
import eventBus from '@/utils/eventBus'

const handleLogin = () => {
  // 发送登录事件
  eventBus.emit('user:login', {
    username: '张三',
    token: 'xxx-token'
  })
}

const addToCart = () => {
  // 发送购物车更新事件
  eventBus.emit('cart:update', 5)
}
</script>

ComponentB.vue (接收方)

vue 复制代码
<!-- ComponentB.vue (接收方) -->
<template>
  <div>
    <h3>组件 B</h3>
    <p>购物车数量: {{ cartCount }}</p>
    <p>当前用户: {{ username || '未登录' }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import eventBus from '@/utils/eventBus'

const cartCount = ref(0)
const username = ref('')

// 监听事件
onMounted(() => {
  // 监听登录事件
  eventBus.on('user:login', (data) => {
    username.value = data.username
    console.log('用户登录:', data)
  })
  
  // 监听购物车更新
  eventBus.on('cart:update', (count) => {
    cartCount.value = count
    console.log('购物车更新:', count)
  })
})

// 组件卸载时取消监听
onUnmounted(() => {
  eventBus.off('user:login')
  eventBus.off('cart:update')
})
</script>

全局注册为 Vue 插件(可选)

plugins/eventBus.ts

ts 复制代码
// plugins/eventBus.ts
import mitt from 'mitt'
import type { App } from 'vue'

const emitter = mitt()

export default {
  install(app: App) {
    app.config.globalProperties.$bus = emitter
  }
}

// main.ts
import eventBusPlugin from './plugins/eventBus'
app.use(eventBusPlugin)

// 在组件中使用
const { $bus } = getCurrentInstance()?.appContext.config.globalProperties
$bus.emit('event', data)

五、全局状态管理(1 种方式)

Pinia --- 官方推荐的状态管理

stores/user.ts

ts 复制代码
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // state
  const token = ref<string | null>(null)
  const userInfo = ref<{ name: string; role: string } | null>(null)
  
  // getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => userInfo.value?.name || '游客')
  
  // actions
  function login(username: string, password: string) {
    // 模拟登录请求
    token.value = 'fake-token'
    userInfo.value = { name: username, role: 'admin' }
    localStorage.setItem('token', token.value)
  }
  
  function logout() {
    token.value = null
    userInfo.value = null
    localStorage.removeItem('token')
  }
  
  return {
    token,
    userInfo,
    isLoggedIn,
    userName,
    login,
    logout
  }
})

任何组件都可以直接使用

vue 复制代码
<!-- 任何组件都可以直接使用 -->
<template>
  <div>
    <div v-if="userStore.isLoggedIn">
      <p>欢迎回来,{{ userStore.userName }}</p>
      <button @click="userStore.logout">退出登录</button>
    </div>
    <div v-else>
      <button @click="userStore.login('张三', '123456')">登录</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

六、其他通信方式

6.1 路由传参

发送方

vue 复制代码
<!-- 发送方 -->
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()

// query 传参(刷新页面不丢失)
router.push({
  path: '/user/detail',
  query: { id: 123, name: '张三' }
})

// params 传参(刷新页面会丢失)
router.push({
  name: 'UserDetail',
  params: { id: 123, name: '张三' }
})
</script>

接收方

vue 复制代码
<!-- 接收方 -->
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

// 获取 query
const userId = route.query.id
const userName = route.query.name

// 获取 params
const userId = route.params.id
const userName = route.params.name
</script>

6.2 浏览器存储

发送方

ts 复制代码
// 发送方
localStorage.setItem('userInfo', JSON.stringify(userInfo))
sessionStorage.setItem('tempData', '临时数据')

// 接收方
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')

// 监听 storage 变化(跨标签页)
window.addEventListener('storage', (e) => {
  if (e.key === 'userInfo') {
    console.log('用户信息已更新', e.newValue)
  }
})

七、实战对比:不同场景选型建议

7.1 场景分析表

场景 推荐方案 原因 代码量
父子组件简单数据传递 Props / Emit 最直接,符合单向数据流
父子组件双向绑定 v-model 语法糖,简洁优雅
父组件调用子组件方法 ref / defineExpose 需要直接操作子组件实例
祖孙组件跨层级传递 provide / inject 避免 props 逐层传递(props drilling)
兄弟组件通信 mitt / Pinia 无直接关系,需要中间人
全局共享状态 Pinia 数据复杂,多处使用
页面间通信 Vue Router 符合路由语义
跨标签页通信 localStorage + storage 事件 浏览器原生支持

7.2 选型决策树

text 复制代码
开始
│
├─ 组件关系?
│  │
│  ├─ 父子
│  │  │
│  │  ├─ 数据单向传递 → props
│  │  ├─ 事件向上传递 → emit
│  │  ├─ 双向绑定 → v-model
│  │  └─ 调用子组件方法 → ref
│  │
│  ├─ 祖孙
│  │  │
│  │  ├─ 数据复杂 → provide/inject
│  │  └─ 需要灵活性 → 作用域插槽
│  │
│  ├─ 兄弟
│  │  │
│  │  ├─ 简单事件 → mitt
│  │  └─ 复杂状态 → Pinia
│  │
│  └─ 任意组件
│     │
│     ├─ 全局状态 → Pinia
│     ├─ 路由跳转 → Vue Router
│     └─ 跨页面持久化 → localStorage

7.3 性能考虑

ts 复制代码
// 1. props 传递大量数据时考虑使用 provide/inject
// ❌ 不好:层层传递
<GrandParent :data="bigData" />
  <Parent :data="bigData" />
    <Child :data="bigData" />

// ✅ 好:直接注入
provide('bigData', bigData)
const bigData = inject('bigData')

// 2. 频繁更新的事件使用节流/防抖
import { debounce } from 'lodash-es'

const handleInput = debounce((value) => {
  emit('update', value)
}, 300)

// 3. Pinia 的 store 使用 storeToRefs 避免失去响应性
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
// 保持响应性
const { userInfo, token } = storeToRefs(userStore)
// actions 可以直接解构
const { login, logout } = userStore

八、面试高频题

Q1: props 和 emit 的底层原理是什么?

ts 复制代码
// 简单来说:
// props 是单向数据流,父组件通过 render 函数将数据传递给子组件
// emit 是通过事件系统,子组件触发父组件绑定的事件

// 源码层面:
// 子组件的 props 会挂载到组件实例的 props 属性上
// emit 本质是调用父组件传递下来的回调函数

Q2: provide/inject 如何实现响应式?

ts 复制代码
// 响应式原理:
// 1. 提供 ref 或 reactive 对象
const state = reactive({ count: 0 })
provide('state', state)

// 2. 后代注入的是同一个响应式对象
const state = inject('state')
// 修改会同步到所有地方
state.count++

// 注意:不要直接 provide 普通值(会失去响应性)
// ❌ provide('count', 0)
// ✅ provide('count', ref(0))

Q3: 兄弟组件通信除了 mitt 还有什么方式?

ts 复制代码
// 1. 通过父组件中转(不推荐,会破坏组件独立性)
// ChildA emit → Parent → props → ChildB

// 2. Pinia/Vuex 全局状态
// 3. 事件总线(mitt 就是这种)
// 4. 浏览器存储 + 事件监听

Q4: 如何设计一个跨组件的主题切换功能?

ts 复制代码
// 方案一:provide/inject
const theme = ref('light')
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
})

// 方案二:Pinia 全局状态
export const useThemeStore = defineStore('theme', {
  state: () => ({ theme: 'light' }),
  actions: {
    toggle() {
      this.theme = this.theme === 'light' ? 'dark' : 'light'
    }
  }
})

// 方案三:CSS 变量 + 根元素类名
document.documentElement.classList.toggle('dark-theme')

九、实战案例:购物车联动

场景描述

  • 商品列表页:展示商品,点击「加入购物车」
  • 悬浮购物车:实时更新数量和总价
  • 购物车页面:展示详细列表
  • 导航栏:显示购物车徽标

stores/cart.ts

ts 复制代码
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
  image: string
}

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  
  // 从 localStorage 恢复
  const saved = localStorage.getItem('cart')
  if (saved) {
    items.value = JSON.parse(saved)
  }
  
  const totalCount = computed(() => 
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )
  
  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  
  function addItem(product: Omit<CartItem, 'quantity'>, quantity = 1) {
    const existing = items.value.find(item => item.id === product.id)
    if (existing) {
      existing.quantity += quantity
    } else {
      items.value.push({ ...product, quantity })
    }
    saveToLocal()
  }
  
  function updateQuantity(id: number, quantity: number) {
    const item = items.value.find(item => item.id === id)
    if (item) {
      item.quantity = Math.max(0, quantity)
      if (item.quantity === 0) {
        items.value = items.value.filter(i => i.id !== id)
      }
    }
    saveToLocal()
  }
  
  function removeItem(id: number) {
    items.value = items.value.filter(item => item.id !== id)
    saveToLocal()
  }
  
  function clearCart() {
    items.value = []
    saveToLocal()
  }
  
  function saveToLocal() {
    localStorage.setItem('cart', JSON.stringify(items.value))
  }
  
  return {
    items,
    totalCount,
    totalPrice,
    addItem,
    updateQuantity,
    removeItem,
    clearCart
  }
})

ProductCard.vue (商品卡片)

vue 复制代码
<!-- ProductCard.vue (商品卡片) -->
<template>
  <div class="product-card">
    <img :src="product.image" :alt="product.name">
    <h4>{{ product.name }}</h4>
    <p>¥{{ product.price }}</p>
    <button @click="addToCart">加入购物车</button>
  </div>
</template>

<script setup lang="ts">
import { useCartStore } from '@/stores/cart'

const props = defineProps<{
  product: { id: number; name: string; price: number; image: string }
}>()

const cartStore = useCartStore()

const addToCart = () => {
  cartStore.addItem(props.product, 1)
  // 可以添加提示
  ElMessage.success('已加入购物车')
}
</script>

NavBar.vue (导航栏徽标)

vue 复制代码
<!-- NavBar.vue (导航栏徽标) -->
<template>
  <div class="navbar">
    <span>商城</span>
    <div class="cart-badge" @click="showCart = true">
      <el-badge :value="cartStore.totalCount" :hidden="cartStore.totalCount === 0">
        <el-icon><ShoppingCart /></el-icon>
      </el-badge>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()
</script>

FloatingCart.vue (悬浮购物车)

vue 复制代码
<!-- FloatingCart.vue (悬浮购物车) -->
<template>
  <el-drawer v-model="visible" title="购物车" size="400px">
    <div v-if="cartStore.items.length === 0" class="empty-cart">
      <el-empty description="购物车是空的" />
    </div>
    
    <div v-else>
      <div v-for="item in cartStore.items" :key="item.id" class="cart-item">
        <img :src="item.image" class="item-img">
        <div class="item-info">
          <div class="item-name">{{ item.name }}</div>
          <div class="item-price">¥{{ item.price }}</div>
        </div>
        <el-input-number 
          v-model="item.quantity" 
          :min="1" 
          size="small"
          @change="(val) => cartStore.updateQuantity(item.id, val)"
        />
        <el-button type="danger" :icon="Delete" link @click="cartStore.removeItem(item.id)" />
      </div>
      
      <div class="cart-footer">
        <div class="total">
          <span>总计:</span>
          <span class="total-price">¥{{ cartStore.totalPrice.toFixed(2) }}</span>
        </div>
        <el-button type="primary" @click="goToCart">去结算</el-button>
      </div>
    </div>
  </el-drawer>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useCartStore } from '@/stores/cart'

const router = useRouter()
const cartStore = useCartStore()
const visible = ref(false)

const goToCart = () => {
  visible.value = false
  router.push('/cart')
}
</script>

十、总结

10.1 8 种通信方式速查表

方式 适用场景 优点 缺点
props 父→子 简单直观,类型安全 只能单向,嵌套深时传递麻烦
emit 子→父 事件驱动,解耦 需要逐层传递
v-model 表单双向绑定 语法糖,简洁 只能绑定一个值(Vue3支持多个)
ref 父调用子方法 直接操作子组件 耦合度高,破坏组件独立性
provide/inject 跨层级传递 避免 props drilling 数据流向不清晰
mitt 兄弟/任意组件 轻量,解耦 需要手动管理监听
Pinia 全局复杂状态 DevTools支持,响应式 学习成本
router 页面间 符合路由语义 仅限页面跳转

10.2 最佳实践建议

  1. 优先使用 props/emit:保持组件独立性
  2. 复杂状态用 Pinia:统一管理,易于调试
  3. 跨层级传递用 provide/inject:避免 props 地狱
  4. 临时事件用 mitt:注意手动清理监听
  5. 表单用 v-model:简洁优雅
  6. 避免滥用 ref:除非必要,不要直接操作子组件

10.3 进阶思考

当你掌握了这些通信方式,就可以思考:

  • 如何封装一个通用的弹窗组件(props + emit + v-model)
  • 如何设计一个权限系统(provide/inject + Pinia)
  • 如何实现一个多标签页应用(Pinia + Vue Router)

记住:选择正确的通信方式,能让代码更优雅、更易维护。希望这篇文章能帮你彻底掌握 Vue3 组件通信!🎉

相关推荐
kyriewen2 小时前
自定义事件:让代码之间也能“悄悄对话”
前端·javascript·面试
子兮曰2 小时前
别把它当成一次普通“源码泄露”:Claude Code 事件给 AI Agent 团队提了什么醒
前端·npm·claude
心之语歌2 小时前
Vue2 data + Vue3 ref/reactive 核心知识点总结
开发语言·前端·javascript
诸葛亮的芭蕉扇2 小时前
tooltip-position-solution
前端·vue.js·elementui
LXXgalaxy2 小时前
`摸鱼决策轮盘`【vue3+ts前端实战小项目】
前端
这是个栗子2 小时前
关于 TypeScript 的介绍
前端·javascript·typescript
亿元程序员3 小时前
亿元Cocos小游戏实战合集指南和答疑
前端
开开心心就好3 小时前
伪装文件历史记录!修改时间的黑科技软件
java·前端·科技·r语言·edge·pdf·语音识别
踩着两条虫3 小时前
AI驱动的Vue3应用开发平台深入探究(十八):扩展与定制之集成第三方库
vue.js·人工智能·低代码·重构·架构