Skip to content
On this page

组件通信

父子组件通信

组件通信是Vue.js应用中最重要的概念之一,它允许组件之间传递数据和事件。Vue.js提供了多种组件通信方式,以满足不同的使用场景。

Props - 父向子传递数据

Props是父组件向子组件传递数据的主要方式。子组件需要显式地声明它期望接收的props。

基础用法

vue
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: {
    title: {
      type: String,
      required: true
    },
    message: {
      type: String,
      default: 'Default message'
    },
    count: {
      type: Number,
      default: 0
    }
  }
}
</script>
vue
<!-- 父组件 ParentComponent.vue -->
<template>
  <div>
    <ChildComponent 
      title="Hello Vue" 
      :message="parentMessage" 
      :count="counter"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: 'Message from parent',
      counter: 5
    }
  }
}
</script>
</template>

Props 验证

Vue.js提供了丰富的props验证选项:

vue
<script>
export default {
  props: {
    // 基础类型检查
    propA: Number,
    
    // 多种可能的类型
    propB: [String, Number],
    
    // 必传的字符串
    propC: {
      type: String,
      required: true
    },
    
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数返回
      default() {
        return { message: 'hello' }
      }
    },
    
    // 自定义验证函数
    propF: {
      validator(value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    
    // 具有默认值的函数
    propG: {
      type: Function,
      default() {
        return 'Default function'
      }
    }
  }
}
</script>

事件 - 子向父传递数据

子组件通过$emit方法触发自定义事件,父组件通过v-on监听这些事件来接收数据。

vue
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <p>Child component: {{ childData }}</p>
    <button @click="sendDataToParent">Send Data to Parent</button>
    <button @click="incrementCounter">Increment</button>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  data() {
    return {
      childData: 'Data from child'
    }
  },
  methods: {
    sendDataToParent() {
      // 触发自定义事件,传递数据给父组件
      this.$emit('child-event', this.childData, 'additional data')
    },
    incrementCounter() {
      this.$emit('increment')
    }
  }
}
</script>
vue
<!-- 父组件 ParentComponent.vue -->
<template>
  <div>
    <p>Parent received: {{ receivedData }}</p>
    <p>Counter: {{ counter }}</p>
    <ChildComponent 
      @child-event="handleChildEvent"
      @increment="handleIncrement"
    />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      receivedData: '',
      counter: 0
    }
  },
  methods: {
    handleChildEvent(data, additionalData) {
      this.receivedData = `${data} - ${additionalData}`
    },
    handleIncrement() {
      this.counter++
    }
  }
}
</script>
</template>

组合式API中的组件通信

在组合式API中,组件通信的方式略有不同:

vue
<!-- 子组件 ChildComponent.vue -->
<script setup>
import { ref } from 'vue'

// 定义props
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 定义事件发射器
const emit = defineEmits(['update-count', 'send-data'])

const localCount = ref(props.count)

function updateParent() {
  // 发射事件
  emit('update-count', localCount.value + 1)
  emit('send-data', 'Data from child component')
}

function increment() {
  localCount.value++
  emit('update-count', localCount.value)
}
</script>

<template>
  <div>
    <h3>{{ title }}</h3>
    <p>Count: {{ localCount }}</p>
    <button @click="updateParent">Update Parent</button>
    <button @click="increment">Increment</button>
  </div>
</template>
vue
<!-- 父组件 ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentCount = ref(0)
const receivedData = ref('')

function handleUpdateCount(newCount) {
  parentCount.value = newCount
}

function handleReceiveData(data) {
  receivedData.value = data
}
</script>

<template>
  <div>
    <p>Parent Count: {{ parentCount }}</p>
    <p>Received: {{ receivedData }}</p>
    <ChildComponent 
      title="Child Component"
      :count="parentCount"
      @update-count="handleUpdateCount"
      @send-data="handleReceiveData"
    />
  </div>
</template>

非父子组件通信

事件总线 (Event Bus)

虽然Vue 3不再推荐使用事件总线,但了解其原理仍有助于理解组件通信:

javascript
// eventBus.js
import { createApp } from 'vue'

const eventBus = createApp({}).config.globalProperties
export default eventBus

Provide / Inject

Provide/inject是Vue提供的一种更优雅的跨层级组件通信方式:

vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const user = ref({
  name: 'John',
  role: 'admin'
})

// 提供响应式数据
provide('theme', theme)
provide('user', user)

function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>

<template>
  <div>
    <button @click="toggleTheme">Toggle Theme</button>
    <ChildComponent />
  </div>
</template>
vue
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

// 注入数据
const theme = inject('theme')
const user = inject('user')
</script>

<template>
  <div :class="theme">
    <p>Current theme: {{ theme }}</p>
    <p>User: {{ user.name }} ({{ user.role }})</p>
  </div>
</template>

使用Vuex进行状态管理

对于复杂的应用,推荐使用状态管理库如Vuex:

javascript
// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0,
    user: null
  },
  mutations: {
    increment(state) {
      state.count++
    },
    setUser(state, user) {
      state.user = user
    }
  },
  actions: {
    increment({ commit }) {
      commit('increment')
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
})

插槽 (Slots)

插槽是Vue中实现内容分发的重要机制,允许父组件向子组件传递模板内容。

默认插槽

vue
<!-- 子组件 CardComponent.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="card-body">
      <slot>Default content</slot>
    </div>
    <div class="card-footer">
      <slot name="footer">Default Footer</slot>
    </div>
  </div>
</template>
vue
<!-- 父组件 -->
<template>
  <CardComponent>
    <template #header>
      <h2>Custom Header</h2>
    </template>
    <p>This is the main content</p>
    <template #footer>
      <button>Submit</button>
    </template>
  </CardComponent>
</template>

作用域插槽

作用域插槽允许子组件向父组件传递数据:

vue
<!-- 子组件 UserList.vue -->
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <slot :user="user" :index="user.index" :is-admin="user.role === 'admin'">
        {{ user.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: 'Alice', role: 'admin' },
        { id: 2, name: 'Bob', role: 'user' },
        { id: 3, name: 'Charlie', role: 'user' }
      ]
    }
  }
}
</script>
vue
<!-- 父组件 -->
<template>
  <UserList>
    <template #default="{ user, isAdmin }">
      <span :class="{ admin: isAdmin }">
        {{ user.name }} - {{ user.role }}
        <span v-if="isAdmin">(Admin)</span>
      </span>
    </template>
  </UserList>
</template>

$refs 和 $parent/$children

使用 $refs

vue
<template>
  <div>
    <input ref="inputRef" type="text">
    <ChildComponent ref="childRef" />
    <button @click="focusInput">Focus Input</button>
    <button @click="callChildMethod">Call Child Method</button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  },
  methods: {
    focusInput() {
      // 访问DOM元素
      this.$refs.inputRef.focus()
    },
    callChildMethod() {
      // 调用子组件方法
      this.$refs.childRef.childMethod()
    }
  }
}
</script>
</template>

在组合式API中使用ref

vue
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

const inputRef = ref(null)
const childRef = ref(null)

onMounted(() => {
  // 访问DOM元素
  inputRef.value.focus()
})

function focusInput() {
  inputRef.value.focus()
}

function callChildMethod() {
  childRef.value.childMethod()
}
</script>

<template>
  <div>
    <input ref="inputRef" type="text">
    <ChildComponent ref="childRef" />
    <button @click="focusInput">Focus Input</button>
    <button @click="callChildMethod">Call Child Method</button>
  </div>
</template>

组件间通信最佳实践

1. 使用类型化的Props (TypeScript)

vue
<script setup lang="ts">
interface Props {
  title: string
  count?: number
  items?: string[]
  callback?: () => void
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => [],
  callback: () => {}
})
</script>

2. 事件命名规范

vue
<!-- 子组件 -->
<script setup>
const emit = defineEmits<{
  'update:modelValue': [value: string]
  'custom-event': [data: any]
}>()

function handleClick() {
  emit('update:modelValue', 'new value')
  emit('custom-event', { message: 'Hello' })
}
</script>

3. 复杂状态管理

对于复杂的应用状态,推荐使用Pinia或Vuex:

vue
<!-- 使用Pinia示例 -->
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
// 解构会保持响应性
const { user, isLoggedIn } = storeToRefs(userStore)

function login() {
  userStore.login()
}
</script>

实际应用示例

表单组件通信

vue
<!-- FormField.vue -->
<script setup>
const props = defineProps({
  label: String,
  modelValue: [String, Number],
  type: {
    type: String,
    default: 'text'
  },
  error: String
})

const emit = defineEmits(['update:modelValue'])

function updateValue(event) {
  emit('update:modelValue', event.target.value)
}
</script>

<template>
  <div class="form-field">
    <label>{{ label }}</label>
    <input 
      :type="type"
      :value="modelValue"
      @input="updateValue"
      :class="{ error: error }"
    >
    <span v-if="error" class="error-message">{{ error }}</span>
  </div>
</template>

<style>
.form-field {
  margin-bottom: 1rem;
}
.error-message {
  color: red;
  font-size: 0.8rem;
}
</style>
vue
<!-- 使用表单字段的组件 -->
<script setup>
import { ref, computed } from 'vue'
import FormField from './FormField.vue'

const formData = ref({
  email: '',
  password: ''
})

const emailError = computed(() => {
  if (!formData.value.email) return ''
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(formData.value.email) ? '' : 'Invalid email'
})

const isFormValid = computed(() => {
  return formData.value.email && 
         emailRegex.test(formData.value.email) &&
         formData.value.password.length >= 6
})

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
</script>

<template>
  <form>
    <FormField 
      label="Email"
      v-model="formData.email"
      type="email"
      :error="emailError"
    />
    <FormField 
      label="Password"
      v-model="formData.password"
      type="password"
    />
    <button type="submit" :disabled="!isFormValid">Submit</button>
  </form>
</template>

组件通信是构建复杂Vue.js应用的基础,通过合理使用各种通信方式,可以构建出结构清晰、易于维护的应用程序。