3.1 props 老爸给儿子传东西

11 阅读9分钟

阶段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 变了,计算结果也要跟着变

场景:父组件传 firstNamelastName,子组件要拼成全名。

操作

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 对它有特殊待遇:

  1. 如果你不传 👉 Vue 自动给个 false
  2. 如果你只写名字<Son is-vip />) 👉 Vue 自动给个 true

代码

defineProps({
    isVip: Boolean
})
  • <Son />isVipfalse
  • <Son is-vip />isViptrue

傻瓜口令:"没传就是假,写名就是真"


阶段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。

傻瓜口令:"新版魔法真厉害,解构也能保链接"