Vue3 中的 Props用法:父子组件通信的核心机制

在 Vue3 中,​​props(属性)​​ 是父组件向子组件传递数据的​​单向通信机制​​,遵循“父→子”的数据流向,是组件复用和模块化的基础。子组件通过声明 props 接收外部数据,父组件通过属性绑定(v-bind 或简写 :)传递数据,确保组件状态的清晰和可控。

一、基础用法:从声明到使用

1. 子组件声明Props

Vue3 提供了两种声明 props 的方式,其中​<script setup> 语法糖​​是最推荐的(更简洁、更符合 Composition API 风格)。

  • ​数组形式​​(简单场景):仅声明 prop 名称,适用于不需要复杂配置的场景。
    // ChildComponent.vue(使用<script setup>)
    const props = defineProps(['title', 'count']) // 声明接收 title(String)和 count(Number)两个 props
  • ​对象形式​​(推荐,复杂场景):可配置类型、默认值、必填性等,适用于需要严格验证的场景。
    const props = defineProps({
      title: {
        type: String,          // 类型:字符串
        required: true,        // 必填
        default: '默认标题'    // 默认值(非必填时生效)
      },
      count: {
        type: Number,          // 类型:数字
        default: 0             // 默认值
      }
    })

2. 父组件传递Props

父组件通过​​属性绑定​​将数据传递给子组件,支持​​静态值​​和​​动态数据​​:

  • ​静态传递​​:直接写死值(适用于固定数据)。
    <!-- ParentComponent.vue -->
    <template>
      <ChildComponent title="静态标题" :count="10" />
    </template>
  • ​动态传递​​:绑定父组件的 datacomputed 属性(适用于变化数据)。
    <template>
      <ChildComponent :title="parentTitle" :count="parentCount" />
    </template>
    <script setup>
    import { ref } from 'vue'
    import ChildComponent from './ChildComponent.vue'
    
    const parentTitle = ref('动态标题') // 父组件 data
    const parentCount = ref(5)         // 父组件 data
    </script>

3. 子组件使用Props

子组件通过​​模板​​或​​script​​访问 props:

  • ​模板中直接使用​​(最常见):
    <template>
      <div>
        <h3>{{ title }}</h3>
        <p>数量:{{ count }}</p>
      </div>
    </template>
  • ​script 中通过 props 访问​​(适用于逻辑处理):
    const props = defineProps(['title'])
    console.log('标题是:', props.title) // 输出父组件传递的 title

二、高级配置:类型验证与默认值

1. 类型验证

通过 type 属性指定 prop 的类型,Vue3 会在开发模式下检查类型是否匹配,不匹配则抛出警告。支持的类型包括:

  • 原生类型:StringNumberBooleanArrayObjectFunctionSymbol
  • 自定义类:通过 instanceof 检查(如 type: Person,需提前定义 Person 类)
const props = defineProps({
  name: String,                  // 字符串
  age: Number,                   // 数字
  isActive: Boolean,             // 布尔值
  hobbies: Array,                // 数组
  profile: Object,               // 对象
  callback: Function,            // 函数
  id: Symbol                     // Symbol
})

2. 默认值与必填性

  • ​默认值​​:通过 default 属性设置,当父组件未传递该 prop 时生效。
    • 基本类型(String、Number 等):直接赋值。
      const props = defineProps({
        count: { type: Number, default: 0 } // 默认值为 0
      })
    • 对象/数组:必须使用​​工厂函数​​返回(避免多个组件实例共享同一引用)。
      const props = defineProps({
        user: { 
          type: Object, 
          default: () => ({ name: '匿名用户', age: 18 }) 
        },
        tags: { 
          type: Array, 
          default: () => ['vue', 'javascript'] 
        }
      })
  • ​必填性​​:通过 required 属性设置,未传递则抛出警告。
    const props = defineProps({
      title: { type: String, required: true } // 必须传递 title
    })

3. 自定义验证函数

通过 validator 函数实现更复杂的验证逻辑(如值的范围、格式等)。函数接收 prop 值作为参数,返回 true(有效)或 false(无效),无效时会抛出警告。

const props = defineProps({
  age: {
    type: Number,
    validator(value) {
      return value >= 0 && value <= 150 // 年龄必须在 0-150 之间
    }
  },
  status: {
    validator(value) {
      return ['active', 'inactive', 'pending'].includes(value) // 状态必须是这三个值之一
    }
  }
})

三、TypeScript 支持:类型安全的Props

Vue3 对 TypeScript 的支持极大提升了 props 的类型安全性。通过泛型和 PropType 工具,可以定义更精确的 props 类型。

1. 使用泛型定义Props

<script setup lang="ts"> 中,通过泛型参数指定 props 的类型:

<script setup lang="ts">
interface User {
  name: string
  age: number
}

const props = defineProps<{
  user: User       // 必填,类型为 User 接口
  theme?: string   // 可选,类型为 string
}>()
</script>

2. 使用 PropType 定义复杂类型

对于自定义类或更复杂的类型,使用 PropType 工具:

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

interface Book {
  name: string
  author: string
}

const props = defineProps({
  book: {
    type: Object as PropType<Book>, // 指定为 Book 类型
    required: true
  },
  onUpdate: {
    type: Function as PropType<(id: number) => void>, // 指定为函数类型
    required: true
  }
})
</script>

四、单向数据流:为什么不能直接修改Props?

Vue3 的​​单向数据流​​原则要求:父组件通过 props 向子组件传递数据,子组件不能直接修改 props。这一设计的目的:

  • ​避免数据混乱​​:如果多个子组件修改同一个 prop,会导致数据来源难以追踪。
  • ​提高可维护性​​:数据变更的路径清晰(父→子),便于调试和维护。

错误示例:直接修改 props

const props = defineProps(['count'])
const increment = () => {
  props.count++ // ❌ 警告:Avoid mutating a prop directly
}

正确做法:通过事件通知父组件

子组件通过 $emit 触发自定义事件,父组件监听事件并修改数据。

<!-- 子组件 ChildComponent.vue -->
<template>
  <button @click="increment">+1</button>
</template>
<script setup>
const props = defineProps(['count'])
const emit = defineEmits(['increment']) // 定义 increment 事件

const increment = () => {
  emit('increment') // 触发 increment 事件
}
</script>
<!-- 父组件 ParentComponent.vue -->
<template>
  <ChildComponent :count="parentCount" @increment="handleIncrement" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentCount = ref(0)

const handleIncrement = () => {
  parentCount.value++ // 父组件修改数据
}
</script>

五、实战案例:待办事项列表组件

场景描述

创建一个​​待办事项列表​​组件,接收待办事项数组(todos)和主题颜色(theme),支持添加和删除待办事项。

代码实现

1. 子组件:TodoList.vue
<template>
  <div :class="['todo-list', theme]">
    <h3>{{ title }}</h3>
    <ul>
      <li v-for="(todo, index) in todos" :key="index">
        {{ todo.text }}
        <button @click="deleteTodo(index)">删除</button>
      </li>
    </ul>
    <input v-model="newTodo" placeholder="添加新待办" />
    <button @click="addTodo">添加</button>
  </div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue'

interface Todo {
  text: string
  completed: boolean
}

const props = defineProps<{
  todos: Todo[]    // 待办事项数组
  title?: string   // 标题(可选)
  theme?: string   // 主题(可选)
}>()

const emit = defineEmits<{
  'update:todos': [todos: Todo[]] // 更新待办事项的事件
}>()

const newTodo = ref('')
const addTodo = () => {
  if (newTodo.value.trim()) {
    const updatedTodos = [...props.todos, { text: newTodo.value, completed: false }]
    emit('update:todos', updatedTodos) // 触发更新事件
    newTodo.value = ''
  }
}

const deleteTodo = (index: number) => {
  const updatedTodos = props.todos.filter((_, i) => i !== index)
  emit('update:todos', updatedTodos) // 触发更新事件
}
</script>
<style scoped>
.todo-list {
  padding: 20px;
  border-radius: 8px;
  margin: 10px;
}
.light { border: 2px solid #ccc; background: #fff; }
.dark { border: 2px solid #333; background: #f5f5f5; color: #333; }
</style>
2. 父组件:App.vue
<template>
  <div>
    <h1>待办事项列表</h1>
    <TodoList 
      :todos="todos" 
      title="我的待办" 
      theme="light"
      @update:todos="updateTodos" 
    />
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import TodoList from './components/TodoList.vue'

const todos = ref([
  { text: '学习 Vue3', completed: false },
  { text: '写一个待办应用', completed: false }
])

const updateTodos = (newTodos: any[]) => {
  todos.value = newTodos // 更新父组件的 todos 数据
}
</script>

六、最佳实践

1. 命名规范

  • ​props 定义​​:使用 camelCase(如 userName),符合 JavaScript 变量命名习惯。
  • ​模板传递​​:使用 kebab-case(如 user-name),因为 HTML 属性不区分大小写。
    <!-- 父组件模板 -->
    <ChildComponent :user-name="name" />

2. 单向数据流

始终通过事件通知父组件修改数据,避免直接修改 props。例如,子组件中需要修改 props 时,使用 emit 触发事件,父组件监听事件并更新数据。

3. 类型安全(TypeScript)

使用 TypeScript 定义 props 类型,提高代码的可维护性和健壮性。例如,通过泛型和 PropType 定义复杂类型,避免类型错误。

4. 性能优化

  • ​避免大型对象​​:不要通过 props 传递大型对象(如包含大量数据的数组),会增加组件渲染成本。
  • ​使用 v-once​:如果 props 数据不会变化,使用 v-once 指令优化渲染性能。
    <ChildComponent :user="staticUser" v-once />

记住:props 是父子组件通信的基础,合理使用能让你的组件更清晰、更易维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值