阶段1:父子关系
目标:理解什么是父组件传值
概念: Vue 开发就像搭积木。 大积木(父组件,比如 App.vue)里面包含小积木(子组件,比如 Son.vue)。 大积木要给小积木传东西(比如传一个标题、传一个颜色),这个传递的数据就叫 Props。
傻瓜口令:"爸爸给儿子传数据,这就叫 Props"
阶段2:收快递 (声明)
目标:使用 defineProps 声明我想要什么
说明:
父组件给你传东西,你得先签收。如果你不说你要什么,Vue 就不给你。
注意:defineProps 是个魔法命令(宏),不需要 import,直接用!
子组件 (Son.vue) 代码:
<script setup>
// 我声明:我只要 'title' 和 'money'
// 用数组的格式写,引号引起来
defineProps(['title', 'money'])
</script>
傻瓜口令:"想要什么先声明,写在数组方框里"
阶段3:拆快递 (模板使用)
目标:在 HTML 模板里显示 Props
说明:
一旦声明了,在 <template> 里就能直接用,跟用 ref 定义的变量一样方便。
子组件 (Son.vue) 代码:
<`template`>
<div class="card">
<!-- 直接用,不需要加 props. -->
<h3>标题:{{ title }}</h3>
<p>零花钱:{{ money }}</p>
</div>
</template>
父组件 (App.vue) 代码:
<`template`>
<!-- 这里的 title=".." 就是传值 -->
<Son title="奥特曼大全" money="50" />
</template>
<script setup>
import Son from './Son.vue' // 别忘了引入子组件
</script>
傻瓜口令:"模板里面直接用,就像自己的一样"
阶段4:手里拿 (脚本使用)
目标:在 <script> JS 代码里使用 Props
说明:
在模板里可以直接用,但在 JS 里,我们得把 defineProps 的结果赋值给一个变量(通常叫 props),然后通过 props.xxx 来拿。
子组件 (Son.vue) 代码:
<script setup>
// 1. 接住返回值
const props = defineProps(['title'])
// 2. 使用 props.title
console.log('爸爸传给我的标题是:', props.title)
</script>
傻瓜口令:"脚本里面要接好,props 点出它的名"
阶段5:只读契约 (Readonly)
目标:Props 是只读的,不可以改
操作: 试着在子组件里修改 Props。
子组件 (Son.vue) 代码:
<script setup>
const props = defineProps(['money'])
const trySteal = () => {
// ❌ 报错!不允许修改!
props.money = 10000
console.log('改不了!')
}
</script>
结果:
控制台警告:Set operation on key "money" failed: target is readonly.
傻瓜口令:"爸爸给的只能看,千万不能动手改"
阶段6:动态进货 (Reactivity)
目标:父组件数据变了,子组件跟着变(单向数据流)
操作:
父组件传一个 ref 变量。
父组件 (App.vue) 代码:
<script setup>
import { ref } from 'vue'
import Son from './Son.vue'
let pocketMoney = ref(100)
</script>
<`template`>
<button @click="pocketMoney += 100">爸爸涨工资</button>
<!-- 用 : (v-bind) 绑定变量 -->
<Son :money="pocketMoney" />
</template>
子组件 (Son.vue) 代码:
<`template`>
<h1>儿子收到的钱:{{ money }}</h1>
</template>
<script setup>
defineProps(['money'])
</script>
结果: 点父组件按钮,子组件显示的钱自动涨。
傻瓜口令:"爸爸变了儿子变,数据流动是单向"
阶段7:实名认证 (String Type)
目标:限制父组件只能传字符串,传别的会警告
操作:
<script setup>
// 以前是 defineProps(['title'])
// 现在改成对象,冒号后面写类型(首字母大写)
defineProps({
title: String
})
</script>
父组件乱传:
<Son :title="123" /> -> 控制台黄色警告:Expected String, got Number.
傻瓜口令:"指定文字类型,乱传可不行"
阶段8:数字游戏 (Number Type)
目标:限制只能传数字,并注意冒号陷阱
操作:
<script setup>
defineProps({
count: Number
})
</script>
⚠️ 冒号陷阱:
<Son count="100" />❌ 传过去的是字符串 "100" (报错)<Son :count="100" />✅ 传过去的是数字 100
傻瓜口令:"只认数字不认人,算数才准确"
阶段9:真假开关 (Boolean Type)
目标:Boolean 类型的特殊简写规则
操作:
<script setup>
defineProps({
isOpen: Boolean
})
</script>
父组件简写:
<Son :is-open="true" />(标准写法)<Son is-open />(推荐写法:只要写了名字,就等于传了 true)
傻瓜口令:"开关不用给值,写上就算开"
阶段10:数组清单 (Array Type)
目标:限制只能传数组
操作:
<script setup>
defineProps({
list: Array
})
</script>
模板使用:
v-for="item in list"
傻瓜口令:"清单要是数组,一串排排坐"
阶段11:对象包裹 (Object Type)
目标:限制只能传对象
操作:
<script setup>
defineProps({
info: Object
})
</script>
模板使用:
{{ info.name }}
傻瓜口令:"对象是个包裹,里面啥都有"
阶段12:必填红星 (Required)
目标:强制父组件必须传值,不能省
说明: 这时候写法又要升级了,冒号后面不光写类型,要写一个对象。
操作:
<script setup>
defineProps({
// 写法升级:对象配置法
title: {
type: String,
required: true // 👈 必填!
}
})
</script>
结果:
如果不传 title,控制台报红:Missing required prop: "title"。
傻瓜口令:"这个必须填,不填会报警"
阶段13:默认备胎 (Default)
目标:父组件不传时,使用默认值
操作:
<script setup>
defineProps({
count: {
type: Number,
default: 1 // 👈 没给就是1
},
message: {
type: String,
default: '暂无消息' // 👈 备胎
}
})
</script>
傻瓜口令:"不传也没事,反正有备胎"
阶段14:工厂生产 (Object Default)
目标:Object 或 Array 的默认值必须是函数 (面试必考)
错误写法:
default: { name: 'Dyl' } ❌
正确写法:
<script setup>
defineProps({
user: {
type: Object,
// 必须写成函数返回的形式
// 就像工厂生产一样,每个组件拿到的都是新的玩具
default: () => ({ name: '游客' })
},
list: {
type: Array,
default: () => [] // 返回空数组
}
})
</script>
傻瓜口令:"对象做备胎,必须用函数"
阶段15:严厉安检 (Validator)
目标:除了类型,还要限制具体的值(比如只能是 'success' 或 'error')
操作:
<script setup>
defineProps({
status: {
type: String,
// 自定义校验函数
validator(value) {
// 只允许这三个值,别的会报警
return ['success', 'warning', 'error'].includes(value)
}
}
})
</script>
演示:
<Son status="happy" /> ❌ 控制台警告:Invalid prop: validation failed.
傻瓜口令:"安检要严格,不符就退货"
阶段16:一键批发 (v-bind object)
目标:把一个对象里的所有属性,一次性全传给子组件
父组件:
<script setup>
import { reactive } from 'vue'
const post = reactive({
id: 1,
title: 'Vue真好玩',
author: 'Dyl'
})
</script>
<`template`>
<!-- ✅ 以前要写三遍: :id="post.id" :title="post.title" ... -->
<!-- 🚀 现在一键搞定: -->
<Son v-bind="post" />
</template>
子组件:
⚠️ 注意:子组件接到的不是 post 对象,而是散开的 id, title, author!
<script setup>
defineProps(['id', 'title', 'author'])
</script>
傻瓜口令:"打包一起传,省心又省力"
阶段17:驼峰烤串 (Casing)
目标:处理命名习惯的冲突(JS喜欢驼峰,HTML喜欢烤串)
子组件 (JS):
defineProps({
userName: String // 🐪 驼峰写法
})
父组件 (HTML):
<!-- 🍢 烤串写法 (推荐) -->
<Son user-name="Dyl" />
<!-- ❌ 驼峰写法在 DOM 模板中可能失效,SFC中可以但通常不用 -->
<!-- <Son userName="Dyl" /> -->
结论:Vue 会自动帮你转换,JS里写驼峰,模板里写烤串,不用担心连不上。
傻瓜口令:"上面用驼峰,下面用烤串"
阶段18:初始种子 (Initial Value)
目标:拿 Props 当初始值,生成一个互不影响的新变量
场景:父组件给个默认 100 分,子组件自己要在内部加减分,不想影响父组件。
操作:
const props = defineProps(['initScore'])
// 📸 拍个快照,存到自己的 ref 里
const myScore = ref(props.initScore)
// 之后修改 myScore,跟 props.initScore 再无关系
myScore.value++
傻瓜口令:"只拿第一次,后面自己管"
阶段19:影子跟随 (Computed)
目标:基于 Props 计算新数据,Props 变了,计算结果也要跟着变
场景:父组件传 firstName 和 lastName,子组件要拼成全名。
操作:
const props = defineProps(['firstName', 'lastName'])
// 🔗 保持连接
const fullName = computed(() => {
return props.firstName + ' ' + props.lastName
})
区别:
- 阶段18 (Ref):断开连接
- 阶段19 (Computed): 保持连接
傻瓜口令:"跟着爸爸变,自动算新值"
阶段20:安全拆包 (toRefs)
目标:解决解构赋值丢失响应性的问题
错误写法:
const props = defineProps(['count'])
const { count } = props // ❌ count 变成了一个普通数字,不再是响应式
正确写法:
import { toRefs } from 'vue'
const props = defineProps(['count'])
// ✅ 将 props 的每个属性都变成 ref
const { count } = toRefs(props)
// 现在 watch(count) 能用了
傻瓜口令:"拆包不失联,还能自动变"
阶段21:手莫伸 (Nested Readonly)
目标:再次强调单向数据流,即使是对象内层属性也不要改
场景:props.info 是一个对象。
操作:
const props = defineProps(['info'])
// ⚠️ 虽然 JS 语法允许修改对象的属性:
// props.info.name = 'New Name'
// 🚫 但这是危险行为!
// 因为这个 info 对象还是父组件里的那个 reactive 对象。
// 你改了,父组件也被你悄悄改了,这叫"数据污染"。
// Vue 开发工具会警告你,或者在以后版本直接封禁。
傻瓜口令:"内层也不能改,数据单向流"
阶段22:布尔变身 (Boolean Casting)
目标:掌握 Boolean 类型的"潜规则"
规则:
有些 Prop 是 Boolean 类型的(比如 is-vip),Vue 对它有特殊待遇:
- 如果你不传 👉 Vue 自动给个
false。 - 如果你只写名字(
<Son is-vip />) 👉 Vue 自动给个true。
代码:
defineProps({
isVip: Boolean
})
<Son />➡isVip是false<Son is-vip />➡isVip是true
傻瓜口令:"没传就是假,写名就是真"
阶段23:双重身份 (Polymorphic Props)
目标:一个 Prop 接受多种类型
场景:用户 ID,有时候是数字 1001,有时候是字符串 "u_1001",我都要支持。
操作:
defineProps({
// 用数组把允许的类型包起来
id: [String, Number]
})
傻瓜口令:"身份能多变,数组来定义"
阶段24:自动吸附 (Attributes Fallthrough)
目标:了解"未声明属性"的去向
现象:
父组件传了 class="red" 和 id="box",但子组件根本没有用 defineProps 声明它们。
这些东西去哪了?
Vue 默认行为:它们会自动"吸附"到子组件的根节点(最外层的那个 div)上。
子组件:
<`template`>
<!-- class="red" 会自动加到这里 -->
<button>点我</button>
</template>
傻瓜口令:"多余属性别担心,自动吸附根节点"
阶段25:分身乏术 (Multi-root Warning)
目标:多根节点导致透传失败
场景: 子组件有两个根节点(Vue 3 允许)。
<`template`>
<button>左边</button>
<button>右边</button>
</template>
问题:
父组件传了 class="red",Vue 懵了:"我有两个儿子(根节点),这贴纸贴谁脑门上?"
结果:控制台黄色警告 Extraneous non-props attributes...,并且样式失效。
傻瓜口令:"两个脑袋也是病,贴纸不知贴哪里"
阶段26:手动贴纸 ($attrs)
目标:解决多根节点透传问题
操作:
告诉 Vue,我想贴在具体哪一个节点上。使用内置变量 $attrs。
代码:
<`template`>
<button>左边</button>
<!-- v-bind="$attrs" 意思是把所有杂七杂八的属性都贴这儿 -->
<button v-bind="$attrs">右边(贴这里)</button>
</template>
傻瓜口令:"指定位置手动贴,想贴哪里贴哪里"
阶段27:拒收贴纸 (inheritAttrs: false)
目标:即使可以自动吸附,我也不想要(比如为了样式安全)
操作:
使用 defineOptions 配置。
代码:
<script setup>
defineProps(['msg'])
// 🚫 拒绝自动透传
defineOptions({
inheritAttrs: false
})
</script>
这样父组件传的 class 就不会自动挂到根节点上了。
傻瓜口令:"拒绝自动乱张贴,我的地盘我做主"
阶段28:脚本检视 (useAttrs)
目标:在 script 代码里看看父组件传了什么杂物
操作:
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs.class) // 能拿到 'red'
console.log(attrs.id) // 能拿到 'box'
注意:attrs 不是响应式的,尽量别用它做动态逻辑。
傻瓜口令:"脚本偷看杂物箱,里面全是未声明"
阶段29:未来魔法 (3.5+ Destructure)
目标:了解 Vue 3.5+ 的 Props 解构新特性
说明:
在 Vue 3.5 版本之前,解构 props 会丢失响应性(阶段20)。
但在 Vue 3.5+ 版本,Vue 施展了魔法,允许直接解构!
代码 (Vue 3.5+):
const { count } = defineProps(['count'])
// 在 3.5+ 里,这个 count 依然是响应式的!
// 还可以直接赋默认值: const { count = 100 } = ...
⚠️ 警告:如果你的项目版本低,千万别这么写,会出 Bug。
傻瓜口令:"新版魔法真厉害,解构也能保链接"