欢迎光临
我们一直在努力

Props与Emit

文章目录

  • 前言
  • 一、Props 基础
    • 1.1 定义
    • 1.2 声明方式
    • 1.3 支持的类型
    • 1.4 TypeScript 写法
  • 二、Emit 基础
    • 2.1 定义
    • 2.2 声明方式
    • 2.3 事件修饰符
  • 三、单向数据流
    • 3.1 核心原则
    • 3.2 常见模式
  • 四、解构 Props 与响应性
    • 4.1 问题
    • 4.2 解决方案
  • 五、应用场景
    • 5.1 列表组件
    • 5.2 表单组件
    • 5.3 通用按钮
    • 5.4 分页组件
  • 六、编译器宏
    • 6.1 defineProps / defineEmits 特性
  • 七、面试聚焦
    • 7.1 解构 Props 失去响应性
    • 7.2 defineProps 需要导入吗?
    • 7.3 Props 类型校验
  • 八、易混淆点
  • 九、思考与练习
  • 总结

前言

Props 和 Emit 是 Vue 最基础的父子通信方式,也是组件接口设计的核心。本篇会讲清楚:

  • defineProps 的类型校验与默认值
  • defineEmits 的事件声明与触发
  • 单向数据流原则
  • v-model 与 Props/Emit 的关系
  • 解构 Props 失去响应性的解决方案

一、Props 基础

1.1 定义

Props 是父组件向子组件传递数据的单向数据通道,子组件通过 defineProps 声明期望接收的属性列表。

<!– 父组件 –>
<script setup>
import Child from './Child.vue'
const title = ref('Hello')
const count = ref(0)
</script>

<template>
<Child :title="title" :count="count" />
</template>

<!– 子组件 Child.vue –>
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
})
</script>

<template>
<h1>{{ title }}: {{ count }}</h1>
</template>

1.2 声明方式

// 1. 数组语法(无类型校验)
defineProps(['title', 'count'])

// 2. 对象语法(推荐,支持校验)
defineProps({
title: String,
count: Number,
disabled: Boolean
})

// 3. 完整对象语法
defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => [] // 对象/数组默认值必须用工厂函数
},
config: {
type: Object,
default: () => ({})
}
})

1.3 支持的类型

defineProps({
// 基础类型
str: String,
num: Number,
bool: Boolean,

// 复合类型
arr: Array,
obj: Object,
fn: Function,

// 多种可能类型
id: [String, Number],

// 自定义校验
age: {
type: Number,
validator: (value) => value >= 0 && value <= 150
}
})

1.4 TypeScript 写法

<script setup lang="ts">
// 类型声明
interface Props {
title: string
count?: number
items?: string[]
}

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


二、Emit 基础

2.1 定义

Emit 是子组件向父组件通信的事件机制,子组件通过 defineEmits 声明可触发的事件列表。

<!– 子组件 –>
<script setup>
const emit = defineEmits(['change', 'submit'])

const handleClick = () => {
emit('change', 42)
}

const handleSubmit = () => {
emit('submit', { name: 'Alice', age: 25 })
}
</script>

<template>
<button @click="handleClick">触发 change</button>
<button @click="handleSubmit">提交</button>
</template>

<!– 父组件 –>
<script setup>
const onChange = (val) => console.log('收到:', val)
const onSubmit = (data) => console.log('提交:', data)
</script>

<template>
<Child @change="onChange" @submit="onSubmit" />
</template>

2.2 声明方式

// 1. 数组语法
defineEmits(['change', 'submit'])

// 2. 对象语法(带校验)
defineEmits({
change: (val) => typeof val === 'number',
submit: (data) => data && typeof data.name === 'string'
})

// 3. TypeScript
const emit = defineEmits<{
change: [value: number]
submit: [data: { name: string; age: number }]
}>()

2.3 事件修饰符

<!– 父组件监听子组件事件 –>
<Child @change.once="handleChange" /> <!– 只触发一次 –>
<Child @submit.prevent="handleSubmit" /> <!– 阻止默认行为 –>


三、单向数据流

3.1 核心原则

// 数据流向:父 → 子(Props)
// 事件流向:子 → 父(Emit)

// ❌ 错误:子组件直接修改 props
const props = defineProps({ count: Number })
props.count++ // 开发模式会警告

// ✅ 正确:通过 emit 通知父组件修改
const emit = defineEmits(['update:count'])
const increment = () => emit('update:count', props.count + 1)

3.2 常见模式

<!– 模式 1:子组件通知父组件更新 –>
<script setup>
const props = defineProps({ value: String })
const emit = defineEmits(['update:value'])

const onInput = (e) => {
emit('update:value', e.target.value)
}
</script>

<template>
<input :value="value" @input="onInput" />
</template>

<!– 模式 2:v-model 语法糖(Vue 3) –>
<!– 子组件 –>
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])

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

<!– 父组件 –>
<MyInput v-model="text" />
<!– 等价于 :modelValue="text" @update:modelValue="text = $event" –>

<!– 模式 3:多个 v-model –>
<script setup>
defineProps({
name: String,
age: Number
})
defineEmits(['update:name', 'update:age'])
</script>

<!– 父组件 –>
<MyForm v-model:name="name" v-model:age="age" />


四、解构 Props 与响应性

4.1 问题

const props = defineProps({ count: Number })

// ❌ 解构后失去响应性
const { count } = props
count // 不会随 props.count 更新

// ❌ 直接解构 defineProps 返回值
const { count } = defineProps({ count: Number })

4.2 解决方案

import { toRefs, toRef } from 'vue'

const props = defineProps({ count: Number, name: String })

// ✅ 方案 1:toRefs 解构所有 props
const { count, name } = toRefs(props)
count.value // 保持响应性

// ✅ 方案 2:toRef 解构单个 prop
const count = toRef(props, 'count')

// ✅ 方案 3:模板中直接使用 props.xxx(无需解构)
// <template>{{ props.count }}</template>

// ✅ 方案 4:computed 派生
const double = computed(() => props.count * 2)


五、应用场景

5.1 列表组件

<!– List.vue –>
<script setup>
defineProps({
items: { type: Array, required: true },
loading: { type: Boolean, default: false }
})
</script>

<template>
<div v-if="loading">加载中…</div>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>

5.2 表单组件

<!– Form.vue –>
<script setup>
const props = defineProps({
modelValue: { type: Object, required: true }
})
const emit = defineEmits(['update:modelValue', 'submit'])

const updateField = (key, value) => {
emit('update:modelValue', { …props.modelValue, [key]: value })
}

const handleSubmit = () => {
emit('submit', props.modelValue)
}
</script>

5.3 通用按钮

<!– Button.vue –>
<script setup>
defineProps({
type: {
type: String,
default: 'primary',
validator: (v) => ['primary', 'danger', 'default'].includes(v)
},
disabled: { type: Boolean, default: false }
})
defineEmits(['click'])
</script>

<template>
<button
:class="type"
:disabled="disabled"
@click="$emit('click', $event)"
>
<slot />
</button>
</template>

5.4 分页组件

<!– Pagination.vue –>
<script setup>
defineProps({
page: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
total: { type: Number, required: true }
})
const emit = defineEmits(['page-change'])

const changePage = (newPage) => {
emit('page-change', newPage)
}
</script>


六、编译器宏

6.1 defineProps / defineEmits 特性

// 1. 编译器宏,无需 import
// ❌ import { defineProps } from 'vue'

// 2. 只能在 <script setup> 顶层使用
// ❌ 不能在条件语句、循环中使用
if (condition) {
defineProps({ }) // 错误
}

// 3. 返回值是只读的响应式对象
const props = defineProps({ count: Number })
// props.count = 1 // 警告,不应修改

// 4. 可在模板中直接使用 prop 名(无需 props. 前缀)
// defineProps({ title: String })
// <template>{{ title }}</template>


七、面试聚焦

7.1 解构 Props 失去响应性

// 问题:解构后 count 是普通值,不会更新
const { count } = defineProps({ count: Number })

// 解决:toRefs
const props = defineProps({ count: Number })
const { count } = toRefs(props)

7.2 defineProps 需要导入吗?

// 不需要。defineProps 和 defineEmits 是编译器宏
// 编译时会被处理,不是运行时函数

7.3 Props 类型校验

// 数组语法无法校验
defineProps(['count']) // 任何类型都能传入

// 对象语法可以校验
defineProps({
count: { type: Number, required: true }
}) // 传入字符串会警告


八、易混淆点

  • Props 只读:子组件不能直接修改 props,应通过 emit 通知父组件。
  • 解构失去响应性:解构 props 需用 toRefs 或 toRef。
  • 默认值工厂函数:对象/数组类型的 default 必须是函数:default: () => ({})。
  • defineProps 是宏:无需 import,不能在条件语句中使用。
  • v-model 本质::modelValue + @update:modelValue 的语法糖。

  • 九、思考与练习

    1. 为什么子组件不能直接修改 props?

    解析:Vue 遵循单向数据流,props 由父组件拥有和控制。子组件修改会破坏数据流的可预测性,应通过 emit 通知父组件修改。

    2. 解构 props 为什么会失去响应性?如何解决?

    解析:解构得到的是普通值快照。使用 toRefs(props) 或 toRef(props, 'key') 保持响应性。

    3. defineProps 和 defineEmits 需要导入吗?

    解析:不需要。它们是编译器宏,在 <script setup> 中直接使用,编译时处理。

    4. v-model 在 Vue 3 中是如何工作的?

    解析:

    <MyInput v-model="text" />
    <!– 等价于 –>
    <MyInput :modelValue="text" @update:modelValue="text = $event" />

    5. 对象类型的 default 为什么必须用工厂函数?

    解析:如果直接写 default: {},所有组件实例会共享同一个对象引用,修改一个会影响其他实例。工厂函数每次创建新对象。


    总结

    • Props:父 → 子,单向数据通道,defineProps 声明
    • Emit:子 → 父,事件机制,defineEmits 声明
    • 单向数据流:子不修改 props,通过 emit 通知父
    • v-model:Props + Emit 的语法糖
    • 解构响应性:用 toRefs / toRef 保持响应性
    • 编译器宏:defineProps / defineEmits 无需 import
    赞(0)
    未经允许不得转载:171主机测评 » Props与Emit
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址