Vue3 Props
Vue3 Props
- 1. 使用方式(defineProps)
- 1.1 使用数组进行定义(简单方式)
- 1.2 使用对象进行声明(可定义字段类型)
- 1.3 省略写法
- 2. 响应式解构(外部函数,比如侦听需要使用getter函数追踪变更)
- 2.1 详细解析
- 2.2 完整示例
- 3. 传递 prop 的细节
- 3.1 prop 名字格式(内部用小驼峰命名法,外部用短线连接命名法)
- 3.2 传递动态的 prop
- 3.3 传递不同类型的 prop(支持所有类型)
- 3.4 使用一个对象绑定多个 prop(推荐,代码更加简洁)
- 4. 单向数据流
- 4.1 子组件直接修改(无效且有警告)
- 4.2 初始值备份(不随prop更新而更新)
- 4.3 prop 的计算属性(随prop更新而更新)
- 4.4 更改对象 / 数组类型的 props(不推荐直接在子组件内部更改)
- 4.4.1 不推荐的原因
- 4.4.2 正确的更改方式
- 5. prop 校验
- 5.1 警告示例
- 5.2 校验规则
- 5.2.1 校验规则示例
- 5.2.2 细节补充
- 5.2.3 运行时类型检查
- 5.2.3.1 支持的原生构造函数
- 5.2.3.2 可为 null 类型
- 5.2.3.3 Boolean 类型转换
1. 使用方式(defineProps)
一个组件希望接受父组件传递来的数据,这些 atrribute 就是props。
在子组件内部,通过 defineProps 宏 声明接收哪些 props,而父组件使用子组件时通过 attribute 传递数据。下面是一个实例。
1.1 使用数组进行定义(简单方式)
子组件 StudentCard:
<template><div class="student-card"><div>姓名:{{ student.name }}</div><div>年龄:{{ student.age }} 岁</div><div>性别:{{ student.gender }}</div><div>当前年级:{{ student.currentGrade }} 年级</div></div>
</template><script setup>
const student = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])
</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
父组件App.vue:
<template><div><StudentCard name="张三" :age="12" gender="男" :current-grade="8"></StudentCard><StudentCard name="孙尚香" :age="14" gender="女" :current-grade="9"></StudentCard></div>
</template><script setup>
import StudentCard from '@/components/StudentCard.vue';
</script><style lang="scss" scoped></style>

1.2 使用对象进行声明(可定义字段类型)
子组件 StudentCard:
<template><div class="student-card"><div>姓名:{{ student.name }}</div><div>年龄:{{ student.age }} 岁</div><div>性别:{{ student.gender }}</div><div>当前年级:{{ student.currentGrade }} 年级</div></div>
</template><script setup>
const student = defineProps({name: String,age: Number,gender: String,currentGrade: Number,
})
</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
1.3 省略写法
事实上,我们可以省略 defineProps 的赋值部分const student =,在子组件中直接使用声明的变量,比如:
<template><div class="student-card"><div>姓名:{{ name }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div></div>
</template><script setup>
defineProps([ 'name', 'age', 'gender', 'currentGrade' ])
</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
或者
<template><div class="student-card"><div>姓名:{{ name }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div></div>
</template><script setup>
defineProps({name: String,age: Number,gender: String,currentGrade: Number,
})
</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
2. 响应式解构(外部函数,比如侦听需要使用getter函数追踪变更)
2.1 详细解析
有时候,我们需要监听子组件中prop的变化,可以使用将其解构出来,通过watch 或者 watchEffect 进行操作。
import { watch } from 'vue';const {currentGrade} = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])watch(() => currentGrade, (newVal) => {console.log(`当前年级变为:${newVal}`)
})
注意:这里将解构出的 prop 包装在了一个 getter 中,这是一个解构出的prop,只是一个值,而非响应式对象。
如果不使用 getter,直接对其进行侦听,则会报错。比如:
watch(currentGrade, (newVal) => {console.log(`当前年级变为:${newVal}`)
})

不过好在报错提示也很友好,有对应的解决方案:
[plugin:vite:vue] [@vue/compiler-sfc] “currentGrade” is a destructured prop and should not be passed directly to watch(). Pass a getter () => currentGrade instead.
2.2 完整示例
完整示例如下。
子组件 StudentCard.vue:
<template><div class="student-card"><div>姓名:{{ name }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div></div>
</template><script setup>
import { watch } from 'vue';const {currentGrade} = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])watch(() => currentGrade, (newVal) => {console.log(`当前年级变为:${newVal}`)
})
</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
父组件 App.vue:
<template><div><StudentCard :name="zhangsan.name" :age="zhangsan.age" :gender="zhangsan.gender" :current-grade="zhangsan.currentGrade"></StudentCard><StudentCard name="孙尚香" :age="14" gender="女" :current-grade="9"></StudentCard></div>
</template><script setup>
import StudentCard from '@/components/StudentCard.vue';
import { ref } from 'vue';const zhangsan = ref({name: '张三',age: 12,gender: '男',currentGrade: 8
})setTimeout(() => {zhangsan.value.currentGrade = 9
}, 2000)
</script><style lang="scss" scoped></style>

3. 传递 prop 的细节
3.1 prop 名字格式(内部用小驼峰命名法,外部用短线连接命名法)
(1)建议声明名字长的 prop 时,采用 camelCase (小驼峰式命名法)。比如:
defineProps({greetingMessage: String
})
(2)在外部向子组件传递 prop 时,建议采用 kebab-case(中间用 - 连接,虽然理论上使用 camelCase 也不会出问题,这么做是为了和 HTML attribute 对齐)。比如:
<MyComponent greeting-message="hello" />
(3)注意区别于组件名的 PascalCase(大驼峰式命名),这有助于提高模板可读性,区分原生的 html 元素。
3.2 传递动态的 prop
比如:
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" /><!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
3.3 传递不同类型的 prop(支持所有类型)
prop 其实支持传递所有类型的 prop 值,让我们看看一些类型的示例和注意点。
- 传递Number:
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" /><!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />
- 传递 Boolean:
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published /><!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :is-published="false" /><!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />
- 传递 Array:
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" /><!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />
- 传递 Object:
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost:author="{name: 'Veronica',company: 'Veridian Dynamics'}"/><!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />
3.4 使用一个对象绑定多个 prop(推荐,代码更加简洁)
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用 v-bind 而非 :prop-name。
比如将 2.2 示例中的 App.vue 进行改造:
<template><div><StudentCard v-bind="zhangsan"></StudentCard></div>
</template><script setup>
import StudentCard from '@/components/StudentCard.vue';
import { ref } from 'vue';const zhangsan = ref({name: '张三',age: 12,gender: '男',currentGrade: 8
})</script><style lang="scss" scoped></style>
4. 单向数据流
Vue 中的 prop 遵循单向数据流原则。
简单说,就是只允许通过修改在父组件修改子组件的 prop 值,而不能由子组件自行修改。从而避免数据流的混乱。
4.1 子组件直接修改(无效且有警告)
让我们在子组件 StudentCard.vue 中尝试修改 prop 的值:
<template><div class="student-card"><div>姓名:{{ name }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div></div>
</template><script setup>
const props = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])setTimeout(() => {props.name = '小明'
}, 1000)</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>

[Vue warn] Set operation on key “name” failed: target is readonly.
发现 prop 在子组件内部是只读的,无法修改。
4.2 初始值备份(不随prop更新而更新)
虽然子组件 prop 在内部是只读的,但是我们可以通过备份一份初始数据,从而进行一些操作。
关键代码:
const props = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])// initialName 只是记录 props.name 的初始值,和后续的修改已经没有关联了
const initialName = ref(props.name)
子组件 StudentCard.vue:
<template><div class="student-card"><div>姓名:{{ name }}</div><div>原名:{{ initialName }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div></div>
</template><script setup>
import { ref } from 'vue'const props = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])const initialName = ref(props.name)setTimeout(() => {initialName.value = '小王'
}, 3000)</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
父组件 App.vue:
<template><div><StudentCard v-bind="zhangsan"></StudentCard></div>
</template><script setup>
import StudentCard from '@/components/StudentCard.vue';
import { ref } from 'vue';const zhangsan = ref({name: '张三',age: 12,gender: '男',currentGrade: 8
})setTimeout(() => {zhangsan.value.name = '小明'
}, 1000)</script><style lang="scss" scoped></style>

1s后,发现备份的 prop 值,和原来的 prop 没有关联了。

3s后

4.3 prop 的计算属性(随prop更新而更新)
需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性。
关键代码:
import { computed } from 'vue'const props = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])const introduction = computed(() => `大家好,我的名字是${props.name}`)
子组件 StudentCard.vue:
<template><div class="student-card"><div>姓名:{{ name }}</div><div>自我介绍:{{ introduction }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div></div>
</template><script setup>
import { computed } from 'vue'const props = defineProps([ 'name', 'age', 'gender', 'currentGrade' ])const introduction = computed(() => `大家好,我的名字是${props.name}`)</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
父组件 App.vue:
<template><div><StudentCard v-bind="zhangsan"></StudentCard></div>
</template><script setup>
import StudentCard from '@/components/StudentCard.vue';
import { ref } from 'vue';const zhangsan = ref({name: '张三',age: 12,gender: '男',currentGrade: 8
})setTimeout(() => {zhangsan.value.name = '小明'
}, 1000)</script><style lang="scss" scoped></style>

1s后,计算属性也随 prop 的更新而更新了。

4.4 更改对象 / 数组类型的 props(不推荐直接在子组件内部更改)
4.4.1 不推荐的原因
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,对 Vue 来说,阻止这种更改需要付出的代价异常昂贵。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
关键代码:

子组件 StudentCard.vue:
<template><div class="student-card"><div>姓名:{{ name }}</div><div>年龄:{{ age }} 岁</div><div>性别:{{ gender }}</div><div>当前年级:{{ currentGrade }} 年级</div><div>同班同学有: {{ classmates?.join('、') }}</div><div>父亲: {{ family?.father }}</div></div>
</template><script setup>const props = defineProps([ 'name', 'age', 'gender', 'currentGrade', 'classmates', 'family' ])setTimeout(() => {props.classmates[0] = '小明'props.classmates[1] = '小王'props.family.father = '张三丰'
}, 2000)</script><style lang="scss" scoped>
.student-card{padding: 10px;margin: 10px;border: 1px solid #ccc;
}
</style>
父组件 App.vue:
<template><div><StudentCard v-bind="zhangsan"></StudentCard></div>
</template><script setup>
import StudentCard from '@/components/StudentCard.vue';
import { ref } from 'vue';const zhangsan = ref({name: '张三',age: 12,gender: '男',currentGrade: 8,classmates: ['小红', '小刚'],family: {father: '张无忌'}
})</script><style lang="scss" scoped></style>

2s后

可以看到,确实可以在子组件中直接修改 prop 中的数组和对象元素内部的值。但是,这是非常不推荐的做法,因为这会让数据流的变更难以理解。
4.4.2 正确的更改方式
正确的做法有两种:
(1)直接在外部父组件修改;
(2)在内部使用 defineEmits 向父组件传递事件和数据变化;
其中(2)可以参考 《Vue3 模板引用——ref》中的 2.2.1 使用 props 和 emit(比较常用,父子通信) 部分
5. prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
5.1 警告示例
子组件 StudentCard.vue:

父组件 App.vue:


5.2 校验规则
5.2.1 校验规则示例
defineProps({// 基础类型检查// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)propA: Number,// 多种可能的类型propB: [String, Number],// 必传,且为 String 类型propC: {type: String,required: true},// 必传但可为 null 的字符串propD: {type: [String, null],required: true},// Number 类型的默认值propE: {type: Number,default: 100},// 对象类型的默认值propF: {type: Object,// 对象或数组的默认值// 必须从一个工厂函数返回。// 该函数接收组件所接收到的原始 prop 作为参数。default(rawProps) {return { message: 'hello' }}},// 自定义类型校验函数// 在 3.4+ 中完整的 props 作为第二个参数传入propG: {validator(value, props) {// The value must match one of these stringsreturn ['success', 'warning', 'danger'].includes(value)}},// 函数类型的默认值propH: {type: Function,// 不像对象或数组的默认,这不是一个// 工厂函数。这会是一个用来作为默认值的函数default() {return 'Default function'}}
})
注意:defineProps() 宏中的参数不可以访问
<script setup>中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
5.2.2 细节补充
(1)所有 prop 默认都是可选的,除非声明了 required: true。
(2)除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined。
(3)Boolean 类型的未传递 prop 将被转换为 false。
这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
(4)如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。
(5)如果是 ts,defineProps<{ msg: string }> 会被编译为 { msg: { type: String, required: true }}。
5.2.3 运行时类型检查
5.2.3.1 支持的原生构造函数
校验选项中的 type 可以是下列这些原生构造函数:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
- Error
5.2.3.2 可为 null 类型
如果该类型是必传但可为 null 的,你可以用一个包含 null 的数组语法:
defineProps({id: {type: [String, null],required: true}
})
注意:如果 type 仅为 null 而非使用数组语法,它将允许任何类型。
5.2.3.3 Boolean 类型转换
为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 组件为例:
defineProps({disabled: Boolean
})
该组件可以被这样使用:
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled /><!-- 等同于传入 :disabled="false" -->
<MyComponent />
当一个 prop 被声明为允许多种类型时,Boolean 的转换规则也将被应用。然而,当同时允许 String 和 Boolean 时,有一种边缘情况——只有当 Boolean 出现在 String 之前时,Boolean 转换规则才适用:
// disabled 将被转换为 true
defineProps({disabled: [Boolean, Number]
})// disabled 将被转换为 true
defineProps({disabled: [Boolean, String]
})// disabled 将被转换为 true
defineProps({disabled: [Number, Boolean]
})// disabled 将被解析为空字符串 (disabled="")
defineProps({disabled: [String, Boolean]
})
上一章 《Vue3 组件注册》
