一、props:和 Vue2 核心逻辑完全一致,仅访问方式微调
props 作为 Vue 「父传子」的核心通信方式
Vue3 中 单向数据流、类型校验、默认值 / 必传项 等核心规则和 Vue2 完全一样,唯一区别是「访问方式」:
1. 共性(Vue2/Vue3 通用)
- 单向数据流:子组件不能直接修改 props,必须通过
emit通知父组件修改; - 支持类型校验( String / Number / Array / Object 等)、默认值、自定义校验规则;
- 父组件传的属性如果没被 props 声明,会落到
attrs中(下文会提)。
2. 用法对比(Vue2 选项式 vs Vue3 组合式)
xml
<!-- Vue2 选项式 API -->
<script>
export default {
props: {
name: { type: String, default: '默认名' },
age: { type: Number, required: true }
},
mounted() {
console.log(this.name); // 👉 通过 this 访问
}
}
</script>
<!-- Vue3 组合式 API(setup) -->
<script>
export default {
// 👉 props 定义规则和 Vue2 完全一样
props: {
name: { type: String, default: '默认名' },
age: { type: Number, required: true }
},
// 👉 props 作为 setup 第一个参数传入,无需 this
setup(props) {
console.log(props.name); // 直接访问
// 注意:props 是响应式的,解构会丢失响应式,需用 toRefs
const { name } = Vue.toRefs(props);
console.log(name.value); // ref 需 .value 访问
}
}
</script>
二、context:Vue3 把 Vue2 的「this 上的通信属性」聚合到上下文
context 是 setup 的第二个参数(非响应式,可直接解构),核心作用是替代 Vue2 中 this 上的「非 props 相关通信能力」,你提到的几个属性对应关系精准,补充用法细节:
| context 属性 | Vue2 对应写法 | 核心用法示例 |
|---|---|---|
| context.emit | this.$emit | 子传父触发事件:context.emit('change', { id: 1 })(父组件用 @change 接收) |
| context.slots | this.$slots | 访问父组件传入的插槽:context.slots.header()(Vue3 插槽是函数,需加 () 调用) |
| context.attrs | this.$attrs | 接收父组件未被 props 声明的属性:父传 class="box" 且 props 未声明 → context.attrs.class |
| context.expose() | 无(Vue2 无此能力) | 主动暴露子组件内部属性给父组件:context.expose({ fn: () => console.log('暴露的方法') }) |
核心示例(context 解构使用,更简洁)
xml
<script>
export default {
setup(props, { emit, slots, attrs, expose }) {
// 1. 子传父:触发自定义事件
const handleClick = () => emit('submit', '子组件数据');
// 2. 访问具名插槽
console.log(slots.footer()); // 获取父组件传入的 footer 插槽内容
// 3. 访问未声明的属性
console.log(attrs['data-id']); // 父传 data-id="123" 且未被 props 声明
// 4. 暴露内部方法给父组件(父通过 ref 仅能访问暴露的内容)
const internalFn = () => '内部逻辑';
expose({ internalFn }); // 父组件 ref.value.internalFn() 可调用
return { handleClick };
}
}
</script>
1. 子组件(Child.vue):核心逻辑详解
xml
<template>
<!-- 点击按钮触发子传父事件 -->
<button @click="handleClick">点击触发submit事件</button>
<!-- 渲染父组件传入的footer具名插槽 -->
<div class="slot-container">
<slot name="footer"></slot>
</div>
<!-- 把attrs中的data-id透传给内部div(演示attrs用法) -->
<div :data-id="attrs['data-id']">透传父组件未声明的data-id属性</div>
</template>
<script>
// 导入vue的核心方法(按需导入,Vue3组合式API规范)
import { toRefs } from 'vue';
export default {
// 第一步:声明props(仅声明name,未声明data-id,所以data-id会落到attrs中)
props: {
name: {
type: String,
default: '默认名称'
}
},
// setup第二个参数解构出:emit(子传父)、slots(插槽)、attrs(透传属性)、expose(暴露内容)
setup(props, { emit, slots, attrs, expose }) {
// 👉 1. 子传父:触发自定义事件(核心用法)
const handleClick = () => {
// 第一个参数:事件名(父组件用@submit接收);第二个参数:传递给父组件的数据
emit('submit', {
msg: '子组件传递的数据',
name: props.name // 结合props使用,把props数据也传给父组件
});
};
// 👉 2. 访问具名插槽(控制台打印插槽内容,验证是否传入)
console.log('===== 访问footer插槽 =====');
// Vue3中slots的每个插槽都是函数,调用后返回VNode数组(插槽的DOM结构)
if (slots.footer) { // 先判断父组件是否传入了footer插槽,避免报错
const footerSlotContent = slots.footer();
console.log('footer插槽的VNode内容:', footerSlotContent);
} else {
console.log('父组件未传入footer插槽');
}
// 👉 3. 访问父组件未被props声明的属性(attrs)
console.log('===== 访问attrs =====');
console.log('父组件传入的data-id:', attrs['data-id']); // 父传的data-id未被props声明,所以在attrs中
console.log('父组件传入的class(若有):', attrs.class); // class/style会自动透传,也会在attrs中
// 注意:attrs是非响应式的,若需要响应式,可结合toRefs(但一般attrs无需响应式)
// 👉 4. 暴露子组件内部方法/属性给父组件(父组件通过ref访问)
// 定义子组件内部方法(未return也能通过expose暴露)
const internalFn = () => {
return `内部方法执行成功!props.name的值是:${props.name}`;
};
// 定义内部属性(仅暴露给父组件,模板中无法直接使用,除非return)
const internalData = '子组件内部私有数据';
// 主动暴露指定内容(只有这里声明的,父组件才能通过ref访问)
expose({
internalFn, // 暴露内部方法
internalData, // 暴露内部属性
// 也可以暴露props(方便父组件直接获取props值)
getPropsName: () => props.name
});
// 👉 5. 补充:props的响应式使用(可选)
// 解构props并保留响应式(若需要单独使用props中的属性)
const { name } = toRefs(props);
console.log('===== props使用 =====');
console.log('props.name的值(响应式):', name.value);
// 把需要在模板中使用的方法return出去(handleClick在模板中绑定点击事件,必须return)
return {
handleClick,
attrs // 把attrsreturn出去,方便模板中使用(如上面模板中的:data-id="attrs['data-id']")
};
}
};
</script>
2. 父组件(Parent.vue):调用子组件并配合使用
xml
<template>
<div class="parent-container">
<h3>父组件</h3>
<!-- 第二步:使用子组件,完成以下操作:
1. 传props:name="测试名称"
2. 传未声明的属性:data-id="10086"(会落到子组件attrs中)
3. 绑定子组件的自定义事件:@submit="handleChildSubmit"
4. 传入具名插槽:<template #footer>...</template>
5. 给子组件加ref:childRef(用于访问子组件暴露的内容)
-->
<Child
ref="childRef"
name="测试名称"
data-id="10086"
class="child-component"
@submit="handleChildSubmit"
>
<!-- 传入footer具名插槽(子组件会访问这个插槽) -->
<template #footer>
<p>这是父组件传给子组件的footer插槽内容</p>
</template>
</Child>
<!-- 显示子组件传递的数据 -->
<div class="child-data" v-if="childSubmitData">
<h4>子组件传递的数据:</h4>
<p>msg:{{ childSubmitData.msg }}</p>
<p>name:{{ childSubmitData.name }}</p>
</div>
<!-- 点击按钮访问子组件暴露的方法/属性 -->
<button @click="accessChildExpose">访问子组件暴露的内容</button>
</div>
</template>
<script>
// 导入子组件
import Child from './Child.vue';
// 导入vue的ref(用于创建子组件的引用)和onMounted(生命周期)
import { ref, onMounted } from 'vue';
export default {
// 注册子组件
components: {
Child
},
setup() {
// 👉 1. 创建子组件的ref引用(用于访问子组件暴露的内容)
const childRef = ref(null);
// 👉 2. 接收子组件的自定义事件数据
const childSubmitData = ref(null);
const handleChildSubmit = (data) => {
console.log('父组件接收到子组件的submit事件数据:', data);
childSubmitData.value = data; // 把数据存到响应式变量中,模板中显示
};
// 👉 3. 访问子组件通过expose暴露的内容
const accessChildExpose = () => {
// 确保子组件已挂载(避免初始时childRef.value为null)
if (childRef.value) {
// 调用子组件暴露的internalFn方法
const fnResult = childRef.value.internalFn();
console.log('调用子组件暴露的internalFn结果:', fnResult);
// 获取子组件暴露的internalData属性
const internalData = childRef.value.internalData;
console.log('获取子组件暴露的internalData:', internalData);
// 调用子组件暴露的getPropsName方法(获取子组件的props.name)
const propsName = childRef.value.getPropsName();
console.log('子组件的props.name:', propsName);
// 注意:子组件未暴露的内容,父组件无法访问(比如子组件的handleClick)
console.log('访问子组件未暴露的handleClick:', childRef.value.handleClick); // undefined
}
};
// 👉 4. 生命周期:组件挂载后,也可以主动访问子组件暴露的内容
onMounted(() => {
console.log('===== 组件挂载后访问子组件暴露内容 =====');
if (childRef.value) {
console.log('挂载后获取internalData:', childRef.value.internalData);
}
});
// return需要在模板中使用的变量/方法
return {
childRef,
childSubmitData,
handleChildSubmit,
accessChildExpose
};
}
};
</script>
三、关键总结
props:规则完全继承 Vue2 ,仅在 Vue3 setup 中需通过第一个参数访问,注意响应式解构(用toRefs);context:替代 Vue2 中 this 上的通信属性 ,把$emit/$slots/$attrs聚合到 上下文( context ) ,新增expose()增强组件封装性(Vue2 父组件通过 ref 能访问子组件所有内容,Vue3 需主动暴露才可见);- 简化记忆:
setup(props, context)→ 第一个参数管「父传子的 props」,第二个参数管「子传父、插槽、透传属性、暴露内容」。
这种设计既保留了 Vue2 的使用习惯,又让组合式 API 脱离了 this 的束缚,逻辑更聚合,是 Vue3 兼顾「易用性」和「灵活性」的核心设计。