props 数据
Props 声明
组件需要显式声明props,这样 Vue 才能知道,外部传入的,哪些是props,哪些是透传attribute(下一章中我们会讨论到它)。
在单文件组件中使用<script setup>
时,props可以使用defineProps()宏来定义:
<script setup> const props = defineProps(['foo']) console.log(props.foo) </script> <template> <div>{{ foo }]</div> </template>
在没有使用<script setup>
的组件中,props需使用props 选项来定义:
<script> export default { props: ['foo'], setup(props) { // setup() 将 props 作为第一个参数 console.log(props.foo) } } </script> <template> <div>{{ foo }]</div> </template>
请注意,传给defineProps()的参数和提供给props 选项的值是相同的,两种声明方式背后其实使用的都是props 选项
。
除了使用字符串数组来声明props外,还可以使用对象的形式:
<script setup>defineProps ({ title: String, likes: Number }) </script>
<script> export default {props : { title: String, likes: Number } } </script>
对于以对象形式声明中的每个属性,key
是 prop 的名称,而值应该是预期类型的构造函数。比如,如果要求一个 prop 的值是number
类型,则可使用Number
构造函数作为其声明的值。
对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。
如果你正在搭配 TypeScript 使用<script setup>
,也可以使用类型标注来声明props:
<script setup lang="ts">defineProps <{ title?: string, likes?: number, }>() </script>
props 传值
prop 名字格式
如果 prop 的名字很长,应使用camelCase形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中引用。避免在作为属性 key 名时必须带引号。
js defineProps({ greetingMessage: String })
template <span>{{ greetingMessage }}</span>
从技术上来讲,你也可以在向子组件传递props时使用camelCase形式。(使用 DOM 模板时例外)而实际上为了和 HTML attribute 对齐,都会将其转为kebab-case形式:
template <MyComponent greeting-message="hello" />
一般情况下都会使用PascalCase作为组件标签名,因为这提高了模板的可读性,能很好地区分出 Vue 组件和原生 HTML 元素。然而这对于传递 props 来说收效却并不高,因此我们选择对其进行转换。
静态 vs.动态 Props
至此,你已经见过了很多像这样的静态值形式的 props:
<BlogPost title="My journey with Vue" />
相应地还有使用v-bind
或缩写:
所动态绑定的 props 值:
<!-- 根据一个变量的值动态传入 --> <BlogPost :title="post.title" /> <!-- 根据一个更复杂表达式的值动态传入 --> <BlogPost :title="post.title + ' by ' + post.author.name" />
props 传值类型
在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 prop。
Number
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue --> <!-- 这是一个 JavaScript 表达式而不是一个字符串。 --> <BlogPost:likes ="42" /> <!-- 用一个变量进行动态赋值。--> <BlogPost:likes ="post.likes" />
Boolean
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。 --> <BlogPostis-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" />
传入一个对象的所有 property
如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的v-bind,而非:prop-name
。例如,对于一个给定的对象post
:
post: { id: 1, title: 'My Journey with Vue' }
下面的模板:
<BlogPost v-bind="post" /> 等价于: <BlogPost v-bind:id="post.id" v-bind:title="post.title" />
单向数据流
所有的 prop 都遵循着单向下行绑定原则,prop 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改了父组件的状态,不然应用的数据流就会变得难以理解了
另外,每次父组件更新后,所有的子组件中的props都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
const props = defineProps(['foo']) // ❌ 警告!prop 是只读的! props.foo = 'bar'
想要更改 prop 通常都符合以下两种场景:
1、prop 用来传递一个初始值;而子组件想在之后希望将其作为一个本地局部的数据来使用。在这种情况下,最好是新定义一个局部数据属性,从props上取初始值即可:
const props =defineProps (['initialCounter']) // 计数器只是将 props.initialCounter 作为初始值 // 像下面这样做就使 prop 和后续更新无关了 const counter = ref(props.initialCounter)
2、prop 以一种原始的值传入,但还需作转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props =defineProps (['size']) // 该 prop 变更时计算属性也会自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())
更改对象/数组类型的 prop
当对象或数组作为 prop 被传入时,虽然子组件无法更改 prop 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改变。不虽然可能,但有很大的性能损耗,比较得不偿失。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响了父组件的状态,可能会使数据流在将来变得更难以推理。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
要描述对 prop 的校验,你可以向defineProps()
宏提供一个带有 prop 校验的对象,而不是一个字符串数组,例如:
defineProps ({ // 基础类型检查 // (给出 `null` 和 `undefined` 值则会跳过任何类型检查) propA: Number, // 多种可能的类型 propB: [String, Number], // 必传,且为 String 类型 propC: {type : String,required : true }, // Number 类型的默认值 propD: {type : Number,default : 100 }, // 对象类型的默认值 propE: {type : Object, // 一个返回默认 对象/数组值 的工厂函数default() {return { message: 'hello' } } }, // 自定义类型校验函数 propF: {validator (value) { // The value must match one of these stringsreturn ['success', 'warning', 'danger'].includes(value) } }, // 函数类型的默认值 propG: {type : Function, // 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数default() {return 'Default function' } } })
defineProps()
宏中的参数不可以访问<script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
一些补充细节:
- 所有 props 默认都是可选的,除非声明了
required: true
。 - 未传递的 prop 会有一个缺省值
undefined
。 - 如果声明了
default
值,那么在 prop 的值被解析为undefined
时,无论 prop 是未被传递还是显式指明的undefined
,都会改为default
值。
当 prop 的校验失败后,Vue 会抛出一个控制台警告(在开发模式下)。
如果使用了基于类型的 prop 声明,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举个例子,defineProps<{msg: string}>
会被编译为{msg:{type: String, required: true}}
。
类型检查
type
可以是下列原生构造函数中的一个:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
此外,type
还可以是一个自定义的构造函数,并且通过instanceof
来检查、断言。例如下面这个类:
class Person {constructor (firstName, lastName) { this.firstName = firstName this.lastName = lastName } }
你可以将其作为一个 prop 的类型:
defineProps ({ author: Person })
这会校验author
prop 的值是否是由new Person
创建的。
Boolean 类型转换
为了更贴近原生Boolean
attributes 的行为,声明为Boolean
类型的 props 有特别的类型转换规则。以带有如下声明的<MyComponent>
组件为例:
defineProps ({ disabled: Boolean })
该组件可以被这样使用:
<!-- 等同于传入 :disabled="true" --> <MyComponent disabled /> <!-- 等同于传入 :disabled="false" --> <MyComponent />
当需要一个 prop 在声明时允许多种类型时,应该像这样:
defineProps ({ disabled: [Boolean, Number] })
无论声明类型的顺序如何,Boolean
类型的特殊转换规则都会被应用。