一、父组件向子组件传参(Props)
核心逻辑
父组件通过自定义属性传递数据,子组件通过 defineProps(组合式 API)/ props 选项(选项式 API)声明接收,Props 是单向数据流(子组件不能直接修改父组件传递的 Props,需通过事件通知父组件修改)。
1. 基础用法(组合式 API / <script setup>)
步骤 1:子组件声明接收 Props
子组件(如 Child.vue)通过 defineProps 声明要接收的参数,支持指定类型、默认值、校验规则:
<!-- Child.vue -->
<template>
<div>
<!-- 直接使用Props -->
<p>父组件传递的字符串:{{ msg }}</p>
<p>父组件传递的数字:{{ count }}</p>
<p>父组件传递的对象:{{ user.name }} - {{ user.age }}</p>
</div>
</template>
<script setup>
// 方式1:简洁写法(仅指定类型)
const props = defineProps(['msg', 'count', 'user']);
// 方式2:完整写法(指定类型、默认值、校验,推荐)
const props = defineProps({
// 字符串类型
msg: {
type: String,
required: true, // 是否必传
default: '默认值' // 非必传时的默认值
},
// 数字类型
count: {
type: Number,
default: 0
},
// 对象类型(默认值需用函数返回,避免引用共享)
user: {
type: Object,
default: () => ({
name: '默认用户',
age: 18
}),
// 自定义校验规则
validator: (value) => {
return value.age >= 0; // 校验年龄必须非负
}
}
});
// 访问Props(组合式API中可直接通过props.xxx访问)
console.log(props.msg);
</script>
步骤 2:父组件传递数据给子组件
父组件(如 Parent.vue)通过自定义属性传递数据,支持静态值、动态绑定(v-bind / :):
<!-- Parent.vue -->
<template>
<div>
<!-- 静态传递(仅字符串) -->
<Child msg="Hello 子组件" />
<!-- 动态传递(绑定变量/表达式) -->
<Child
:msg="parentMsg"
:count="parentCount"
:user="parentUser"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import Child from './Child.vue';
// 父组件数据
const parentMsg = ref('父组件的动态消息');
const parentCount = ref(100);
const parentUser = reactive({
name: '张三',
age: 25
});
</script>
2. 选项式 API 写法(兼容 Vue2 习惯)
<!-- Child.vue(选项式API) -->
<template>
<p>{{ msg }}</p>
</template>
<script>
export default {
// 声明Props
props: {
msg: {
type: String,
required: true
}
},
mounted() {
console.log(this.msg); // 访问Props
}
};
</script>
3. 注意事项
- 单向数据流:子组件不能直接修改 Props(修改会触发警告),若需修改,需通过「子传父」通知父组件修改源数据;
- 默认值规则:对象 / 数组类型的默认值必须用「函数返回」,避免多个组件实例共享同一个引用;
- Props 校验:类型支持 String/Number/Boolean/Array/Object/Date/Function/Symbol,也支持自定义构造函数;
- Props 命名 :父组件传递时建议用 kebab-case(如
:user-name="name"),子组件声明时用 camelCase(userName),Vue 会自动转换。
二、子组件向父组件传参(自定义事件)
核心逻辑
子组件通过 emit 触发自定义事件并传递数据,父组件通过 v-on(@)监听事件并接收数据,实现子向父的通信。
1. 基础用法(组合式 API / <script setup>)
步骤 1:子组件触发自定义事件
子组件通过 defineEmits 声明事件,再通过 emit 触发并传递数据:
<!-- Child.vue -->
<template>
<div>
<!-- 点击按钮触发事件 -->
<button @click="handleClick">向父组件传值</button>
<button @click="handleSendObj">传递对象数据</button>
</div>
</template>
<script setup>
// 方式1:简洁写法(声明事件名)
const emit = defineEmits(['change', 'send-obj']);
// 方式2:完整写法(校验事件参数,可选)
const emit = defineEmits({
// 校验change事件的参数(必须是数字)
change: (value) => {
return typeof value === 'number';
},
sendObj: (obj) => {
return obj.name && obj.age;
}
});
// 触发事件并传递数据
const handleClick = () => {
// emit(事件名, 传递的参数1, 参数2, ...)
emit('change', 666);
};
const handleSendObj = () => {
emit('send-obj', { name: '李四', age: 30 });
};
</script>
步骤 2:父组件监听事件并接收数据
父组件通过 @事件名 监听子组件的自定义事件,通过回调函数接收数据:
<!-- Parent.vue -->
<template>
<div>
<p>子组件传递的数字:{{ childNum }}</p>
<p>子组件传递的对象:{{ childObj.name }}</p>
<!-- 监听子组件事件 -->
<Child
@change="handleChildChange"
@send-obj="handleChildObj"
/>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import Child from './Child.vue';
const childNum = ref(0);
const childObj = reactive({ name: '', age: 0 });
// 接收子组件的数字数据
const handleChildChange = (num) => {
childNum.value = num;
console.log('子组件传递的数字:', num); // 输出 666
};
// 接收子组件的对象数据
const handleChildObj = (obj) => {
Object.assign(childObj, obj);
console.log('子组件传递的对象:', obj); // 输出 { name: '李四', age: 30 }
};
</script>
2. 选项式 API 写法
<!-- Child.vue(选项式API) -->
<template>
<button @click="handleClick">传值给父组件</button>
</template>
<script>
export default {
// 声明要触发的事件(可选)
emits: ['change'],
methods: {
handleClick() {
// 通过this.$emit触发事件
this.$emit('change', 888);
}
}
};
</script>
<!-- Parent.vue(选项式API) -->
<template>
<Child @change="handleChange" />
</template>
<script>
import Child from './Child.vue';
export default {
components: { Child },
data() {
return { childNum: 0 };
},
methods: {
handleChange(num) {
this.childNum = num;
}
}
};
</script>
3. 注意事项
- 事件命名 :子组件
emit时建议用 kebab-case(如send-obj),父组件监听时保持一致; - 多参数传递:若需传递多个参数,可打包成对象(推荐)或数组,避免参数混乱;
- 事件校验 :
defineEmits支持校验事件参数,不符合规则时控制台会警告; - 避免原生事件冲突 :自定义事件名不要和原生事件(如
click、input)重名。
三、父子组件双向绑定(v-model)
Vue3 支持自定义组件的 v-model,本质是「Props + 自定义事件」的语法糖,简化双向绑定逻辑。
1. 基础双向绑定
子组件声明 Props 和事件(默认:modelValue + update:modelValue)
<!-- Child.vue -->
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
// 声明默认的v-model Props
const props = defineProps(['modelValue']);
// 声明对应的更新事件
const emit = defineEmits(['update:modelValue']);
</script>
父组件使用 v-model 绑定
<!-- Parent.vue -->
<template>
<div>
<p>双向绑定的值:{{ inputValue }}</p>
<Child v-model="inputValue" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const inputValue = ref('初始值');
</script>
2. 多个 v-model 绑定
Vue3 支持给组件绑定多个 v-model,只需自定义 Props 和事件名:
<!-- Child.vue -->
<template>
<input
type="text"
:value="name"
@input="$emit('update:name', $event.target.value)"
/>
<input
type="number"
:value="age"
@input="$emit('update:age', $event.target.value)"
/>
</template>
<script setup>
const props = defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);
</script>
<!-- Parent.vue -->
<template>
<Child
v-model:name="userName"
v-model:age="userAge"
/>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const userName = ref('张三');
const userAge = ref(25);
</script>
四、父子组件访问实例(非推荐,应急使用)
Vue3 不推荐直接访问组件实例,但特殊场景下可通过 ref + expose 实现:
1. 父组件访问子组件实例
子组件暴露方法 / 数据(defineExpose)
<!-- Child.vue -->
<script setup>
import { ref } from 'vue';
const childCount = ref(0);
const addCount = () => {
childCount.value++;
};
// 暴露给父组件的属性/方法(不暴露则父组件无法访问)
defineExpose({
childCount,
addCount
});
</script>
父组件通过 ref 访问
<!-- Parent.vue -->
<template>
<Child ref="childRef" />
<button @click="handleAccessChild">访问子组件</button>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const childRef = ref(null); // 子组件实例引用
const handleAccessChild = () => {
// 访问子组件暴露的属性
console.log(childRef.value.childCount);
// 调用子组件暴露的方法
childRef.value.addCount();
};
</script>
2. 子组件访问父组件实例(不推荐)
<!-- Child.vue -->
<script setup>
import { getCurrentInstance } from 'vue';
// 获取组件实例(仅应急使用,易耦合)
const instance = getCurrentInstance();
// 访问父组件实例
const parentInstance = instance.parent;
console.log(parentInstance.props); // 父组件Props
console.log(parentInstance.vnode.props); // 父组件传递的属性
</script>
五、核心总结
| 通信方向 | 实现方式 | 核心 API / 语法 | 适用场景 |
|---|---|---|---|
| 父 → 子 | Props | defineProps / props | 父组件向子组件传递初始数据 |
| 子 → 父 | 自定义事件 | defineEmits / $emit | 子组件通知父组件修改数据 |
| 双向绑定 | v-model(Props + 事件) | v-model / update:xxx | 父子组件数据双向同步 |
| 实例访问 | ref + expose | defineExpose / getCurrentInstance | 应急访问组件方法 / 数据(慎用) |
最佳实践
- 优先使用「Props + 自定义事件」,遵循单向数据流,降低组件耦合;
- 复杂场景(如跨多级组件、非父子组件)建议使用 Pinia / Provide/Inject,而非多层 Props / 事件;
- Props 只做数据传递,子组件不要修改 Props,通过事件让父组件修改源数据;
- 避免过度使用组件实例访问(ref + expose),易导致代码维护性下降。