一个组件需要显示声明它所接受的props,这样vue才能知道外部传入的哪些是props,哪些是透传attribute
在使script setup的单文件中,props可以使用 ==defineProps()==宏来声明:
<script setup>
const props= defineProps(['foo'])
console.log(props.foo)
<script>
在没有使用script setup的组件中,prop可以使用props选项来声明:
export default{
props:['foo'],
setup(props){
//接收props作为第一个参数
console.log(props.foo)
}
}
注意传递给defineProps的参数和提供给props选项的值是相同的,两种声明方式背后其实使用的都是prop选项。
除了使用字符串数组来声明prop外,还可以使用对象的形式:
//使用
defineProps({
title:String,
likes:Number
}
}
//使用非
export default{
props:{
title:String,
likes:Nunber
}
}
对于以对象形式声明中的每个属性,key是prop的名称,而值则是该prop预期类型的构造函数。比如,如果要求一个prop的值是number类型,则可以使用Number构造函数作为其声明的值。
对象形式的props声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时,传递错误的类型,也会在浏览器控制台中抛出警告。我们将在本章节稍后进一步讨论有关 prop 校验的更多细节。
如果你正在搭配 TypeScript 使用
<script setup lang='ts'>
defineProps<{
title?:string
likes?:number
}>()
</script>
如果一个prop的名字很长,应使用camelCase形式,因为它们是合法的Javascript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性key名时必须加上引号
defineProps({
greetingMessage: String
})
<span>{{ greetingMessage }}</span>
虽然理论上你也可以在向子组件传递props时使用camelCase形式(使用Dom 内模板是例外),但实际上为了和HTML attribute 对齐,我们通常会为其写为 kebab-case形式:
<MyComponent greeting-message='hello'/>
对于组件名我们推荐使用 PascalCase ,因为这提高了模板的可读性,能帮助我们区分vue组件和原生Html元素。对于传递props来说,使用camelCase并没有太多优势,因此我们推荐更贴切html的书写风格。
至此,你已经见过了很多像这样的静态值形式的props:
<BlogPost title='My journey whit vue'/>
相应地,还有使用v-bind 或 缩写 :来动态绑定的props:
<BlogPost :title='post.title' />
<BlogPost :title="post.title+ 'by'+post.author.name"/>
在上述的两个例子中,我们只传入了字符串值,但实际上任何类型的值都可以作为props的值被传递。
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />
<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost
:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
/>
<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />
如果你想要将一个对象的所有属性都当作props传入,你可以使用没有参数的v-bind,即只使用 v-bind 而非属性
const post = {
id: 1,
title: 'My Journey with Vue'
}
以及下面的模板:
<BlogPost v-bind="post" />
而这实际上等价于:
<BlogPost :id="post.id" :title="post.title" />
所有的props都遵循着单向绑定原则,props因父组件的更新而变化,自然地将新状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将会很容易变得混乱而难以理解。
另外,每次父组件更新完后,所有的子组件中的props都会被更新到最新值,这意味着你不应该在子组件中去修改一个prop。否则会抛出警告:
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())
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。