← 返回主页

第7课: 组件通信

Props和Emits(父子通信)

父组件通过props向子组件传递数据,子组件通过emits向父组件发送事件。

<!-- Child.vue -->
<template>
  <div>
    <p>接收到: {{ message }}</p>
    <button @click="sendToParent">发送给父组件</button>
  </div>
</template>

<script setup>
const props = defineProps(['message'])
const emit = defineEmits(['response'])

function sendToParent() {
  emit('response', '来自子组件的消息')
}
</script>
<!-- Parent.vue -->
<template>
  <div>
    <Child :message="parentMsg" @response="handleResponse" />
  </div>
</template>

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

const parentMsg = ref('来自父组件的消息')

function handleResponse(msg) {
  console.log(msg)
}
</script>

Provide/Inject(跨层级通信)

用于祖先组件向所有后代组件传递数据,无论层级多深。

祖先组件提供数据

<!-- Grandparent.vue -->
<template>
  <div>
    <Parent />
  </div>
</template>

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

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

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

// 提供方法
provide('updateTheme', (newTheme) => {
  theme.value = newTheme
})
</script>

后代组件注入数据

<!-- Grandchild.vue -->
<template>
  <div>
    <p>主题: {{ theme }}</p>
    <p>用户: {{ user.name }}</p>
    <button @click="changeTheme">切换主题</button>
  </div>
</template>

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

// 注入数据
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')

// 提供默认值
const config = inject('config', { timeout: 3000 })

function changeTheme() {
  updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>
注意:provide/inject主要用于深层组件通信,对于简单的父子通信,使用props/emits更合适。

模板引用(ref)

通过ref直接访问子组件实例或DOM元素。

访问子组件

<!-- Child.vue -->
<template>
  <div>子组件</div>
</template>

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

const count = ref(0)

// 暴露给父组件
defineExpose({
  count,
  increment() {
    count.value++
  }
})
</script>
<!-- Parent.vue -->
<template>
  <div>
    <Child ref="childRef" />
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

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

const childRef = ref(null)

function callChildMethod() {
  childRef.value.increment()
  console.log(childRef.value.count)
}

onMounted(() => {
  // 组件挂载后可以访问
  console.log(childRef.value)
})
</script>

事件总线(Event Bus)

Vue3移除了$on、$off等方法,可以使用第三方库或自己实现。

使用mitt库

// eventBus.js
import mitt from 'mitt'

export const emitter = mitt()
<!-- ComponentA.vue -->
<template>
  <button @click="sendMessage">发送消息</button>
</template>

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

function sendMessage() {
  emitter.emit('message', { text: 'Hello from A' })
}
</script>
<!-- ComponentB.vue -->
<template>
  <div>{{ receivedMessage }}</div>
</template>

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

const receivedMessage = ref('')

function handleMessage(data) {
  receivedMessage.value = data.text
}

onMounted(() => {
  emitter.on('message', handleMessage)
})

onUnmounted(() => {
  emitter.off('message', handleMessage)
})
</script>

Vuex/Pinia(状态管理)

用于大型应用的全局状态管理,将在后续课程详细讲解。

// store.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '张三',
    age: 25
  }),
  actions: {
    updateName(newName) {
      this.name = newName
    }
  }
})
<!-- 任何组件都可以访问 -->
<script setup>
import { useUserStore } from './store'

const userStore = useUserStore()

console.log(userStore.name)
userStore.updateName('李四')
</script>

v-model多个绑定

<!-- UserForm.vue -->
<template>
  <div>
    <input
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)"
    >
    <input
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)"
    >
  </div>
</template>

<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
<!-- 使用 -->
<template>
  <UserForm
    v-model:first-name="first"
    v-model:last-name="last"
  />
  <p>{{ first }} {{ last }}</p>
</template>

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

const first = ref('')
const last = ref('')
</script>

Attrs透传

<!-- MyButton.vue -->
<template>
  <button v-bind="$attrs">
    <slot />
  </button>
</template>

<script setup>
// 禁用自动继承
defineOptions({
  inheritAttrs: false
})
</script>
<!-- 使用 -->
<template>
  <MyButton class="primary" @click="handleClick">
    点击我
  </MyButton>
</template>

实战示例:表单组件通信

<!-- FormInput.vue -->
<template>
  <div class="form-input">
    <label>{{ label }}</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      @blur="$emit('blur')"
    >
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>

<script setup>
defineProps(['label', 'modelValue', 'error'])
defineEmits(['update:modelValue', 'blur'])
</script>
<!-- Form.vue -->
<template>
  <form @submit.prevent="handleSubmit">
    <FormInput
      v-model="form.username"
      label="用户名"
      :error="errors.username"
      @blur="validateUsername"
    />
    <FormInput
      v-model="form.email"
      label="邮箱"
      :error="errors.email"
      @blur="validateEmail"
    />
    <button type="submit">提交</button>
  </form>
</template>

<script setup>
import { reactive } from 'vue'
import FormInput from './FormInput.vue'

const form = reactive({
  username: '',
  email: ''
})

const errors = reactive({
  username: '',
  email: ''
})

function validateUsername() {
  errors.username = form.username.length < 3 ? '用户名至少3个字符' : ''
}

function validateEmail() {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  errors.email = !emailRegex.test(form.email) ? '邮箱格式不正确' : ''
}

function handleSubmit() {
  validateUsername()
  validateEmail()
  if (!errors.username && !errors.email) {
    console.log('提交表单', form)
  }
}
</script>

组件通信方式总结

练习

  1. 创建一个父子组件,实现数据的双向传递
  2. 使用provide/inject实现主题切换功能
  3. 创建一个表单组件库,包含输入框、选择框等子组件
  4. 使用ref实现父组件调用子组件的方法