02 Props
Props 声明
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是 05 透传 Arrtibutes。
在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:
1 | <script setup> |
在没有使用 <script setup> 的组件中,props 可以使用 props 选项来声明:
1 | export default { |
注意传递给 defineProps() 的参数和提供给 props 选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。
除了使用字符串数组来声明 props 外,还可以使用对象的形式:
1 | // 使用 <script setup> |
// 非 <script setup>
1 | export default { |
对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。
对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。
如果你正在搭配 TypeScript 使用 <script setup>,也可以使用类型标注来声明 props:
1 | <script setup lang="ts"> |
响应式 Props 解构
Vue 的响应系统基于属性访问跟踪状态的使用情况。例如,在计算属性或侦听器中访问 props.foo 时,foo 属性将被跟踪为依赖项。
因此,在以下代码的情况下:
1 | const { foo } = defineProps(['foo']) |
在 3.4 及以下版本,foo 是一个实际的常量,永远不会改变。在 3.5 及以上版本,当在同一个 <script setup> 代码块中访问由 defineProps 解构的变量时,Vue 编译器会自动在前面添加 props.。因此,上面的代码等同于以下代码:
1 | const props = defineProps(['foo']) |
此外,你可以使用 JavaScript 原生的默认值语法声明 props 默认值。这在使用基于类型的 props 声明时特别有用。
1 | const { foo = 'hello' } = defineProps<{ foo?: string }>() |
如果你希望在 IDE 中在解构的 props 和普通变量之间有更多视觉上的区分,Vue 的 VSCode 扩展提供了一个设置来启用解构 props 的内联提示。
将解构的 props 传递到函数中
当我们将解构的 prop 传递到函数中时,例如:
1 | const { foo } = defineProps(['foo']) |
这并不会按预期工作,因为它等价于 watch(props.foo, ...)——我们给 watch 传递的是一个值而不是响应式数据源。实际上,Vue 的编译器会捕捉这种情况并发出警告。
与使用 watch(() => props.foo, ...) 来侦听普通 prop 类似,我们也可以通过将其包装在 getter 中来侦听解构的 prop:
1 | watch(() => foo, /* ... */) |
此外,当我们需要传递解构的 prop 到外部函数中并保持响应性时,这是推荐做法:
1 | useComposable(() => foo) |
外部函数可以调用 getter 来追踪提供的 prop 变更。例如,在计算属性或侦听器的 getter 中。
传递 prop 的细节
Prop 名字格式
如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。
1 | defineProps({ |
1 | <span>{{ greetingMessage }}</span> |
虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 ,但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式:
1 | <MyComponent greeting-message="hello" /> |
对于组件名我们推荐使用 PascalCase,因为这提高了模板的可读性,能帮助我们区分 Vue 组件和原生 HTML 元素。然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此我们推荐更贴近 HTML 的书写风格。
静态 vs. 动态 Props
至此,你已经见过了很多像这样的静态值形式的 props:
1 | <BlogPost title="My journey with Vue" /> |
相应地,还有使用 v-bind 或缩写 : 来进行动态绑定的 props:
1 | <!-- 根据一个变量的值动态传入 --> |
传递不同的值类型
在上述的两个例子中,我们只传入了字符串值,但实际上任何类型的值都可以作为 props 的值被传递。
Number
1 | <!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind --> |
Boolean
1 | <!-- 仅写上 prop 但不传值,会隐式转换为 `true` --> |
Array
1 | <!-- 虽然这个数组是个常量,我们还是需要使用 v-bind --> |
Object
1 | <!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind --> |
使用一个对象绑定多个 prop
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用 v-bind 而非 :prop-name。例如,这里有一个 post 对象:
1 | const post = { |
以及下面的模板:
1 | <BlogPost v-bind="post" /> |
而这实际上等价于:
1 | <BlogPost :id="post.id" :title="post.title" /> |
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
1 | const props = defineProps(['foo']) |
导致你想要更改一个 prop 的需求通常来源于以下两种场景:
- prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
1
2
3
4const props = defineProps(['initialCounter'])
// 计数器只是将 props.initialCounter 作为初始值
// 像下面这样做就使 prop 和后续更新无关了
const counter = ref(props.initialCounter) - 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
1
2
3const props = defineProps(['size'])
// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
更改对象 / 数组类型的 props
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,对 Vue 来说,阻止这种更改需要付出的代价异常昂贵。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。
要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象,例如:
1 | defineProps({ |
TIPdefineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
一些补充细节:
- 所有 prop 默认都是可选的,除非声明了
required: true。 - 除
Boolean外的未传递的可选 prop 将会有一个默认值undefined。 Boolean类型的未传递 prop 将被转换为false。这可以通过为它设置default来更改——例如:设置为default: undefined将与非布尔类型的 prop 的行为保持一致。- 如果声明了
default值,那么在 prop 的值被解析为undefined时,无论 prop 是未被传递还是显式指明的undefined,都会改为default值。
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
如果使用了基于类型的 prop 声明 ,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说,defineProps<{ msg: string }>会被编译为{ msg: { type: String, required: true }}。
运行时类型检查
校验选项中的 type 可以是下列这些原生构造函数:
StringNumberBooleanArrayObjectDateFunctionSymbolError
另外,type也可以是自定义的类或构造函数,Vue 将会通过instanceof来检查类型是否匹配。例如下面这个类:
1 | class Person { |
你可以将其作为一个 prop 的类型:
1 | defineProps({ |
Vue 会通过 instanceof Person 来校验 author prop 的值是否是 Person 类的一个实例。
可为 null 的类型
如果该类型是必传但可为 null 的,你可以用一个包含 null 的数组语法:
1 | defineProps({ |
注意如果 type 仅为 null 而非使用数组语法,它将允许任何类型。
Boolean 类型转换
为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 <MyComponent> 组件为例:
1 | defineProps({ |
该组件可以被这样使用:
1 | <!-- 等同于传入 :disabled="true" --> |
当一个 prop 被声明为允许多种类型时,Boolean 的转换规则也将被应用。然而,当同时允许 String 和 Boolean 时,有一种边缘情况——只有当 Boolean 出现在 String 之前时,Boolean 转换规则才适用:
1 | // disabled 将被转换为 true |