Vue3 13.组件02 Props
Props
如果现在正在构建一个博客,可能需要一个表示博客文章的组件。在博客文章上希望所有的博客都使用相同的视觉布局,但是内容是不同的。要实现这样的效果,自然必须向组件中传递数据,例如每篇文章的标题和内容,这就会使用到props。
props是一种特别的attributes,可以在组件上声明注册。要传递给博客文章组件一个标题,必须在组件的props列表上声明它。这里需要用到defineProps
宏:
<!-- BlogPost.vue --> |
defineProps
是一个仅<script setup>
中可用的编译宏命令,并不需要显式地导入。声明的props会自动暴露给模板。defineProps
会返回一个对象,其中包含了可以传递给组件的所有props:
const props = defineProps(['title']) |
如果没有使用<script setup>
,props必须以props
选项的形式声明,props对象会作为setup()
函数的第一个参数被传入:
export default { |
传递给
defineProps
的参数和提供给props
选项的值是相同的,两种声明方式背后其实使用的都是props选项。
除了使用字符串数组的形式来声明props外,还可以使用对象的形式:
//使用 <script setup> |
对于以对象形式声明的每个属性,key是prop的名称,而值则是该prop预期类型的构造函数。比如要求的prop的值是number
类型,那么可以使用Number
构造函数作为其声明的值。
对象形式的props声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在控制台上抛出警告。
如果恰好使用了Typescript,也可以通过使用类型标注来声明props:
<script setup lang="ts"> |
响应式Props解构
Vue的响应系统基于属性访问跟踪状态的使用情况。比如在计算属性或者监听器中访问props.foo
时,foo
属性会被跟踪为依赖项。
const {foo} = defineProps(['foo']) |
在3.4及以下版本,foo
是一个实际的常量,永远不会改变。在3.5及以上版本,当在同一个<script setup>
代码块中访问由defineProps
解构的变量时,Vue编译器会自动在前面添加props.
。
因此以上代码等同于:
const props = defineProps(['foo']) |
此外可以使用Javascript原生默认值语法声明props默认值:
const {foo = 'hello'} = defineProps<{foo?: string}>() |
在3.4及以下的版本中,因为Vue编译器没有自动在解构props前添加
props.
,需要使用withDefaults
编译器宏来辅助实现默认值
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
将解构的props传递到函数中
当将解构的props传递到函数中时,如:
const { foo } = defineProps(['foo']) |
这并不会按预期工作,因为它等价于watch(props.foo, ...)
,因为给watch
传递的是一个值而不是响应式数据源,针对这种情况Vue编译器会捕捉并发出警告。
与使用watch(() => props.foo, ...)
来监听普通prop类似,也可以通过将其包装在getter中来监听解构的prop:
watch(() => foo, /* ... */) |
此外,当需要传递解构的prop到外部函数中并保持响应性时,推荐做法是:
useComposable(() => foo) |
外部函数可以调用getter来追踪提供的prop变更。
Props的一些细节
Prop名字格式
如果一个prop的名字很长,应使用camelCase形式,可以直接在模板的表达式中使用,也可以避免在作为属性key名时必须加上引号。
defineProps({ |
<span>{{ greetingMessage }}</span> |
虽然理论上可以向子组件传递props时使用camelCase形式,但实际上为了和HTML attribute对齐,通常会将其写为kebab-case形式:
<MyComponent greeting-message="hello" /> |
对于组件名推荐使用PascalCase,因为提高了模板的可读性,能帮助区分Vue组件和原生HTML元素。然而对于传递props来说,使用camelCase并没有多大优势,因此推荐使用更贴近HTML的书写风格。
静态Props和动态Props
静态值形式的props:
<BlogPost title="My journey with Vue" /> |
相应地还有使用v-bind
或者缩写:
来进行动态绑定的props:
<!-- 根据一个变量的值动态传入 --> |
传递不同的值类型
上面的例子只演示了传入字符串值,实际上props可以使用任何类型的值作为值。
<!-- Number --> |
使用一个对象绑定多个Prop
如果想要将一个对象的所有属性都当作props传入,可以使用没有参数的v-bind
,即只使用v-bind
而非:prop-name
。比如:
const post = { |
和
<BlogPost v-bind="post" /> |
上面的等价于:
<BlogPost :id="post.id" :title="post.title" /> |
单向数据流
所有的props都遵循着单向绑定原则,props因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
每当父组件更新后,所有的子组件中的props都会被更新到最新值,这意味着不应该在子组件中去更改一个prop。如果这样做,Vue会在控制台上抛出警告。
导致想要更改一个prop的需求通常来源于两种场景:
- prop被用于传入初始值,而子组件想在以后将其作为一个局部数据属性。这种情况下,最好是新定义一个局部数据属性,从props上获取初始值即可:
const props = defineProps(['initialCounter']) |
- 需要对传入的prop值做进一步的转换。在这种情况中,最好是基于该prop值定义一个计算属性:
const props = defineProps(['size']) |
Prop校验
Vue组件可以更细致地声明对传入的props的校验要求。比如上面看到的类型声明,如果传入的值不满足类型要求,Vue会在控制台中抛出警告来提醒。
要声明对props的校验,可以向defineProps()
宏提供一个带有props校验选项的对象,如:
defineProps({ |
defineProps
宏中的参数不可以访问<script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
其他的一些细节:
- 所有的prop都是默认可选的,除非声明了
required: true
。 - 除
Boolean
外的未传递的可选prop将会有一个默认值undefined
。 Boolean
类型的未传递prop将被转换为false
。这可以通过为它设置default
来更改。- 如果声明了
default
值,那么在prop的值被解析为undefined
时,无论prop是未被传递还是显式指明的undefined
,都会被改为default
值。
可为null的类型
如果该类型是必传但可为null的,可以使用一个包含null
的数组:
defineProps({ |
注意⚠️:如果
type
仅为null
,而非数组的话,它将允许任何类型。
运行时类型检查
校验选项中的type
可以是下面的原生构造函数:
String
Number
Boolean
Array
Object
Date
Function
Symbol
Error
另外,type
也可以是自定义的类或者构造函数,Vue将会通过instanceof
来检查类型是否匹配,如:
class Person { |
可以将其作为一个prop类型:
defineProps({ |
Vue会通过instanceof Person
来校验author
prop的值是否是Person
类的一个实例。
Boolean类型转换
为了更加贴近原生boolean attributes的行为,声明为Boolean
类型的props有特别的类型转换规则。如:
defineProps({ |
该组件可以这样使用:
<!-- 等同于传入 :disabled="true" --> |
当一个prop被声明为允许多种类型时,Boolean
的转换规则也将被应用。然而,当同时允许String
和Boolean
时,只有当Boolean
出现在String
之前时,Boolean
转换规则才适用:
//disabled将被转换为true |