vue3 通信,你能说出几种方式❓

203 阅读5分钟

在现代前端开发中,组件化已成为构建复杂应用的核心思想。Vue作为一款流行的渐进式JavaScript框架,提供了多种灵活的组件通信方式。本文将详细介绍Vue中7种常用的组件通信方法,并通过实际示例演示它们的应用场景和使用技巧。

组件通信方式概览

在Vue应用中,组件通信主要有以下7种方式:

  • props - 父组件向子组件传递数据
  • emit - 子组件向父组件发送事件
  • v-model - 双向数据绑定语法糖
  • refs - 直接访问组件实例或DOM元素
  • provide/inject - 跨层级组件数据传递
  • eventBus - 全局事件总线
  • vuex/pinia - 状态管理工具

下面我们将重点介绍前5种方式,并通过一个简单的待办事项列表示例来演示它们的实际应用。

示例场景说明

在我们的示例中,将实现一个简单的待办事项应用:

  • 父组件:显示任务列表
  • 子组件:包含输入框和添加按钮

1. Props方式 - 父传子

Props是Vue中最常见的父组件向子组件传递数据的方式。

父组件代码

<template>
  <!-- 子组件 -->
  <child-components :list="list"></child-components>
  
  <!-- 父组件 -->
  <div class="child-wrap input-group">
    <input
      v-model="value"
      type="text"
      class="form-control"
      placeholder="请输入任务"
    />
    <div class="input-group-append">
      <button @click="handleAdd" class="btn btn-primary" type="button">
        添加
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'

const list = ref(['JavaScript', 'HTML', 'CSS'])
const value = ref('')

// 添加任务的处理函数
const handleAdd = () => {
  list.value.push(value.value)
  value.value = '' // 清空输入框
}
</script>

子组件代码

<template>
  <ul class="parent list-group">
    <li class="list-group-item" v-for="item in props.list" :key="item">
      {{ item }}
    </li>
  </ul>
</template>

<script setup>
import { defineProps } from 'vue'

const props = defineProps({
  list: {
    type: Array,
    default: () => [],
  },
})
</script>

2. Emit方式 - 子传父

Emit允许子组件向父组件发送事件,是实现子传父通信的标准方式。

子组件代码

<template>
  <div class="child-wrap input-group">
    <input
      v-model="value"
      type="text"
      class="form-control"
      placeholder="请输入任务"
    />
    <div class="input-group-append">
      <button @click="handleSubmit" class="btn btn-primary" type="button">
        添加
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, defineEmits } from 'vue'

const value = ref('')
const emits = defineEmits(['add']) // 定义可发出的事件

const handleSubmit = () => {
  emits('add', value.value) // 发出add事件并传递值
  value.value = '' // 清空输入框
}
</script>

父组件代码

<template>
  <!-- 父组件 -->
  <ul class="parent list-group">
    <li class="list-group-item" v-for="item in list" :key="item">
      {{ item }}
    </li>
  </ul>
  
  <!-- 子组件 -->
  <child-components @add="handleAdd"></child-components>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'

const list = ref(['JavaScript', 'HTML', 'CSS'])

// 处理添加事件
const handleAdd = (value) => {
  list.value.push(value)
}
</script>

3. v-model方式 - 双向绑定

v-model是Vue提供的语法糖,可以简化双向数据绑定的实现。

子组件代码

<template>
  <div class="child-wrap input-group">
    <input
      v-model="value"
      type="text"
      class="form-control"
      placeholder="请输入任务"
    />
    <div class="input-group-append">
      <button @click="handleAdd" class="btn btn-primary" type="button">
        添加
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, defineProps, defineEmits } from 'vue'

const value = ref('')

const props = defineProps({
  list: {
    type: Array,
    default: () => [],
  },
})

const emits = defineEmits(['update:list']) // 固定格式事件

// 添加任务
const handleAdd = () => {
  const arr = [...props.list] // 创建新数组以避免直接修改props
  arr.push(value.value)
  emits('update:list', arr) // 发出更新事件
  value.value = '' // 清空输入框
}
</script>

父组件代码

<template>
  <!-- 父组件 -->
  <ul class="parent list-group">
    <li class="list-group-item" v-for="item in list" :key="item">
      {{ item }}
    </li>
  </ul>
  
  <!-- 子组件 -->
  <child-components v-model:list="list"></child-components>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'

const list = ref(['JavaScript', 'HTML', 'CSS'])
</script>

注意update:*是Vue中的固定写法,*表示props中的属性名。

4. Refs方式 - 直接访问

Refs允许父组件直接访问子组件的实例或方法。

父组件代码

<template>
  <ul class="parent list-group">
    <li class="list-group-item" v-for="item in childRefs?.list" :key="item">
      {{ item }}
    </li>
  </ul>
  
  <!-- 子组件 ref的值与script中的保持一致 -->
  <child-components ref="childRefs"></child-components>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponents from './child.vue'

const childRefs = ref(null) // 创建与组件ref同名的响应式引用
</script>

子组件代码

<template>
  <div class="child-wrap input-group">
    <input
      v-model="value"
      type="text"
      class="form-control"
      placeholder="请输入任务"
    />
    <div class="input-group-append">
      <button @click="handleAdd" class="btn btn-primary" type="button">
        添加
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, defineExpose } from 'vue'

const list = ref(['JavaScript', 'HTML', 'CSS'])
const value = ref('')

// 添加任务
const handleAdd = () => {
  list.value.push(value.value)
  value.value = '' // 清空输入框
}

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

注意<script setup>语法糖默认是关闭的,需要通过defineExposeAPI显式暴露需要公开的属性和方法。

5. Provide/Inject方式 - 跨层级传递

Provide/Inject这对API可以实现祖先组件向后代组件传递数据,无论中间隔了多少层级。

父组件代码

<template>
  <!-- 子组件 -->
  <child-components></child-components>
  
  <!-- 父组件 -->
  <div class="child-wrap input-group">
    <input
      v-model="value"
      type="text"
      class="form-control"
      placeholder="请输入任务"
    />
    <div class="input-group-append">
      <button @click="handleAdd" class="btn btn-primary" type="button">
        添加
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, provide, readonly } from 'vue'
import ChildComponents from './child.vue'

const list = ref(['JavaScript', 'HTML', 'CSS'])
const value = ref('')

// 向所有后代组件提供数据
provide('list', readonly(list.value)) // 使用readonly保护数据

// 添加任务
const handleAdd = () => {
  list.value.push(value.value)
  value.value = '' // 清空输入框
}
</script>

子组件代码

<template>
  <ul class="parent list-group">
    <li class="list-group-item" v-for="item in list" :key="item">
      {{ item }}
    </li>
  </ul>
</template>

<script setup>
import { inject } from 'vue'

// 接收祖先组件提供的数据
const list = inject('list')
</script>

最佳实践:使用provide传递数据时,建议使用readonly包装数据,避免子组件意外修改父级数据,保持数据的单向流。

6. Event Bus 方式

eventBus.js

import mitt from 'mitt'
export const emitter = mitt()

发布组件

<template>
  <div class="container">
    <h3>Event Bus方式</h3>
    <task-list />
    <div class="input-group">
      <input v-model="taskInput" placeholder="输入任务" />
      <button @click="addTask">添加</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { emitter } from './eventBus'

const taskInput = ref('')

const addTask = () => {
  if (taskInput.value.trim()) {
    emitter.emit('task-added', taskInput.value.trim())
    taskInput.value = ''
  }
}
</script>

订阅组件 TaskList.vue

<template>
  <ul class="task-list">
    <li v-for="(task, index) in tasks" :key="index">
      {{ task }}
    </li>
  </ul>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { emitter } from './eventBus'

const tasks = ref(['Node.js', 'Express', 'MongoDB'])

onMounted(() => {
  emitter.on('task-added', (task) => {
    tasks.value.push(task)
  })
})

onUnmounted(() => {
  emitter.off('task-added')
})
</script>

7. Vuex/Pinia 方式

store/taskStore.js

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useTaskStore = defineStore('tasks', () => {
  const tasks = ref(['TypeScript', 'Webpack', 'Vite'])
  
  const addTask = (task) => {
    if (task.trim()) {
      tasks.value.push(task.trim())
    }
  }
  
  return { tasks, addTask }
})

使用Pinia的组件

<template>
  <div class="container">
    <h3>Pinia方式</h3>
    <ul class="task-list">
      <li v-for="(task, index) in taskStore.tasks" :key="index">
        {{ task }}
      </li>
    </ul>
    <div class="input-group">
      <input v-model="taskInput" placeholder="输入任务" />
      <button @click="addTask">添加</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useTaskStore } from './store/taskStore'

const taskStore = useTaskStore()
const taskInput = ref('')

const addTask = () => {
  taskStore.addTask(taskInput.value)
  taskInput.value = ''
}
</script>

总结与选择建议

每种通信方式都有其适用场景:

  1. Props - 简单的父传子数据传递,最常用
  2. Emit - 子组件向父组件发送事件,处理用户交互
  3. v-model - 简化表单元素的双向绑定,语法糖便捷
  4. Refs - 需要直接访问子组件实例或方法时使用
  5. Provide/Inject - 跨多层组件传递数据,避免 prop 逐级传递
  6. Event Bus - 非父子组件通信,小型项目简单场景
  7. Vuex/Pinia - 复杂应用状态管理,多组件共享状态

在实际开发中,应根据具体场景选择最合适的通信方式。

简单场景优先使用props/emit,
复杂状态管理考虑Pinia/Vuex,
跨层级通信使用provide/inject。

正确选择组件通信方式可以使代码更加清晰、可维护性更高,同时也能提高应用的性能。