Props

如果现在正在构建一个博客,可能需要一个表示博客文章的组件。在博客文章上希望所有的博客都使用相同的视觉布局,但是内容是不同的。要实现这样的效果,自然必须向组件中传递数据,例如每篇文章的标题和内容,这就会使用到props。

props是一种特别的attributes,可以在组件上声明注册。要传递给博客文章组件一个标题,必须在组件的props列表上声明它。这里需要用到defineProps宏:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
<h4>{{ title }}</h4>
</template>

defineProps是一个仅<script setup>中可用的编译宏命令,并不需要显式地导入。声明的props会自动暴露给模板。defineProps会返回一个对象,其中包含了可以传递给组件的所有props:

const props = defineProps(['title'])

console.log(props.title)

如果没有使用<script setup>,props必须以props选项的形式声明,props对象会作为setup()函数的第一个参数被传入:

export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}

传递给defineProps的参数和提供给props选项的值是相同的,两种声明方式背后其实使用的都是props选项。

除了使用字符串数组的形式来声明props外,还可以使用对象的形式:

//使用 <script setup>
defineProps({
title: String,
likes: Number
})


// 非 <script setup>
export default {
props: {
title: String,
likes: Number
}
}

对于以对象形式声明的每个属性,key是prop的名称,而值则是该prop预期类型的构造函数。比如要求的prop的值是number类型,那么可以使用Number构造函数作为其声明的值。

对象形式的props声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在控制台上抛出警告。

如果恰好使用了Typescript,也可以通过使用类型标注来声明props:

<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>

响应式Props解构

Vue的响应系统基于属性访问跟踪状态的使用情况。比如在计算属性或者监听器中访问props.foo时,foo属性会被跟踪为依赖项。

const {foo} = defineProps(['foo'])

watchEffect(() => {
//在3.5之前只会运行一次
//在3.5之后的版本 foo的每次变化都会触发运行
console.log(foo)
})

在3.4及以下版本,foo是一个实际的常量,永远不会改变。在3.5及以上版本,当在同一个<script setup>代码块中访问由defineProps解构的变量时,Vue编译器会自动在前面添加props.
因此以上代码等同于:

const props = defineProps(['foo'])

watchEffect(() => {
//`foo`由编译器转为`props.foo`
console.log(props.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(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({
greetingMessage: String
})
<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:

<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />

<!-- 根据一个更加复杂的表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />

传递不同的值类型

上面的例子只演示了传入字符串值,实际上props可以使用任何类型的值作为值。

<!-- Number -->
<BlogPost :likes="42" />
<BlogPost :likes="post.likes" />

<!-- Boolean -->
<!-- 仅写上prop名,但是不传值,会被隐式转为`true` -->
<BlogPost is-published />
<BlogPost :is-published="false" />
<BlogPost :is-published="post.isPublished" />

<!-- Array -->
<BlogPost :comment-ids="[234, 266, 273]" />
<BlogPost :comment-ids="post.commentIds" />

<!-- Object -->
<BlogPost
:author="{
name: 'Veronica',
company: 'Veridan Dynamics'
}"
/>
<BlogPost :author="post.author" />

使用一个对象绑定多个Prop

如果想要将一个对象的所有属性都当作props传入,可以使用没有参数的v-bind,即只使用v-bind而非:prop-name。比如:

const post = {
id: 1,
title: "My Journey with Vue"
}

<BlogPost v-bind="post" />

上面的等价于:

<BlogPost :id="post.id" :title="post.title" />

单向数据流

所有的props都遵循着单向绑定原则,props因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

每当父组件更新后,所有的子组件中的props都会被更新到最新值,这意味着不应该在子组件中去更改一个prop。如果这样做,Vue会在控制台上抛出警告。

导致想要更改一个prop的需求通常来源于两种场景:

  1. prop被用于传入初始值,而子组件想在以后将其作为一个局部数据属性。这种情况下,最好是新定义一个局部数据属性,从props上获取初始值即可:
const props = defineProps(['initialCounter'])

//计数器只是将 props.initialCounter 作为初始值
//像下面这样做就使prop和后续更新无关了
const counter = ref(props.initialCounter)
  1. 需要对传入的prop值做进一步的转换。在这种情况中,最好是基于该prop值定义一个计算属性:
const props = defineProps(['size'])

// 该prop变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())

Prop校验

Vue组件可以更细致地声明对传入的props的校验要求。比如上面看到的类型声明,如果传入的值不满足类型要求,Vue会在控制台中抛出警告来提醒。

要声明对props的校验,可以向defineProps()宏提供一个带有props校验选项的对象,如:

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) {
//propG的值必须匹配数组中的某个字符串
return ['success', 'warning', 'danger'].includes(value)
}
},
//函数类型带默认值
propH: {
type: Function,
//不像对象或者数组的默认,这不是一个工厂函数。这是一个被用来当作默认值的函数
default() {
return 'Default Function'
}
}
})

defineProps宏中的参数不可以访问<script setup>中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。

其他的一些细节:

  • 所有的prop都是默认可选的,除非声明了required: true
  • Boolean外的未传递的可选prop将会有一个默认值undefined
  • Boolean类型的未传递prop将被转换为false。这可以通过为它设置default来更改。
  • 如果声明了default值,那么在prop的值被解析为undefined时,无论prop是未被传递还是显式指明的undefined,都会被改为default值。

可为null的类型

如果该类型是必传但可为null的,可以使用一个包含null的数组:

defineProps({
id: {
type: [String, null],
required: true
}
})

注意⚠️:如果type仅为null,而非数组的话,它将允许任何类型。

运行时类型检查

校验选项中的type可以是下面的原生构造函数:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
  • Error

另外,type也可以是自定义的类或者构造函数,Vue将会通过instanceof来检查类型是否匹配,如:

class Person {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
}

可以将其作为一个prop类型:

defineProps({
author: Person
})

Vue会通过instanceof Person来校验authorprop的值是否是Person类的一个实例。

Boolean类型转换

为了更加贴近原生boolean attributes的行为,声明为Boolean类型的props有特别的类型转换规则。如:

defineProps({
disabled: Boolean
})

该组件可以这样使用:

<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />

<!-- 等同于传入 :disabled="false" -->
<MyComponent />

当一个prop被声明为允许多种类型时,Boolean的转换规则也将被应用。然而,当同时允许StringBoolean时,只有当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]
})