Appearance
Props
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。
封装文章组件
vue
<!-- BlogArticle.vue -->
<template>
<div class="article">
<div class="art-top">
<span class="art-type">随笔</span>
<h3 class="title">文章标题</h3>
<span class="pub-time">2023年 5月 12日</span>
<p class="summary">
前两天,服务器经历了一次崩溃重装。服务器的崩溃导致了博客无法访问、数据丢失、escook 小程序停服,同时很多线上 API 接口服务被迫终止。 经过两天的重装处理之后,博客和 escook
小程序已基本恢复正常。此次事件…
</p>
<button class="btn-more">阅读更多</button>
</div>
<div class="art-bottom">
<span class="author">作者:周星驰</span>
<div class="art-bottom__right">
<span class="cmt">62 评论</span>
<span class="favor">★取消收藏</span>
<span class="favor">☆收藏</span>
</div>
</div>
</div>
</template>
<script setup></script>
<style scoped>
.article {
padding: 15px;
box-sizing: border-box;
border-bottom: 1px solid #efefef;
}
.art-top {
display: flex;
flex-direction: column;
align-items: center;
.art-type {
font-size: 12px;
font-weight: bold;
color: #ca9b52;
}
.title {
font-size: 30px;
font-weight: normal;
margin: 5px;
}
.pub-time {
font-size: 13px;
font-style: italic;
color: #a1a1a1;
}
.summary {
font-size: 13px;
line-height: 2em;
color: #464646;
margin: 20px 0;
text-align: justify;
}
.btn-more {
color: #ca9b52;
background-color: #fff;
border: 1px solid #ca9b52;
font-weight: bold;
font-size: 11px;
padding: 10px 25px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
}
.art-bottom {
display: flex;
justify-content: space-between;
font-size: 12px;
font-style: italic;
margin-top: 20px;
.author {
color: #464646;
}
.cmt {
color: #a1a1a1;
}
.favor {
color: #464646;
margin-left: 15px;
}
}
</style>
并在 App.vue 中使用此文章组件:
vue
<!-- App.vue -->
<script setup>
import BlogArticle from './components/BlogArticle.vue'
</script>
<template>
<h1>博客文章列表</h1>
<hr />
<BlogArticle />
<BlogArticle />
</template>
声明 Props
在封装自定义组件时,往往需要渲染动态的数据,而非把数据写死到组件中。因此,那些需要让外界传入到组件中的数据,可以声明为 Props。
例如,把文章的标题声明为 BlogArticle
组件的 Props,这样我们在使用 BlogArticle
组件时就可以传入文章的标题,从而在实现 UI 结构复用的前提下渲染不同的数据。
声明 Props 的语法格式
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明:
vue
<script setup>
// defineProps() 是 Vue3 内置的宏函数,无需导入即可使用,且只能在 <script setup> 中使用
// defineProps() 可以接收一个数组,数组中的每一项都是字符串,表示 prop 的名称
// defineProps() 的返回值是一个对象,包含了所有接收到的 Props 的数据
const props = defineProps(['title'])
// 在 JS 中需要通过 defineProps 返回的 props 对象,来访问具体的 prop 值
console.log(props.title)
</script>
<template>
<!-- 在 template 中,直接根据 prop 名称访问其数据 -->
<h3 class="title">{{ title }}</h3>
</template>
传入 Props 的值
在使用声明了 Props 的自定义组件时,可以通过标签属性的方式传入 Props 的值:
html
<script setup>
import BlogArticle from './components/BlogArticle.vue'
</script>
<template>
<h1>博客文章列表</h1>
<hr />
<!-- 在使用 BlogArticle 组件时,传入 title 属性的值 -->
<BlogArticle title="论持久战" />
<BlogArticle title="星星之火可以燎原" />
</template>
Props 的命名格式
在 SFC 组件中,通过 defineProps()
宏函数声明 Props 时,如果一个 prop 的名字很长,应使用 camelCase(小驼峰) 的形式:
vue
<script setup>
const props = defineProps(['title', 'articleType'])
// 小驼峰的 prop 名称是合法的 js 标识符
console.log(props.articleType)
</script>
<template>
<!-- 小驼峰标识符,在模板中也可以被直接使用 -->
<span class="art-type">{{ articleType }}</span>
<h3 class="title">{{ title }}</h3>
</template>
而在传入 Props 的属性值时,建议把 camelCase(小驼峰)的 prop 名称写成 kebab-case(连字符) 的形式:
html
<script setup>
import BlogArticle from './components/BlogArticle.vue'
</script>
<template>
<h1>博客文章列表</h1>
<hr />
<!-- 传入 prop 值时,推荐采用“连字符”的形式提供 prop 名称(和 HTML 的属性规范保持一致) -->
<BlogArticle title="论持久战" article-type="军事" />
<BlogArticle title="星星之火,可以燎原" article-type="文摘" />
</template>
总结
组件名采用大驼峰(PascalCase)命名法;声明 prop 时采用小驼峰(camelCase)命名法;传入 prop 值时,采用连字符(kebab-case)命名法。
静态和动态 Props
TIP
静态 Props 和动态 Props 的典型区别:是否使用 v-bind
指令修饰 prop。
静态 Props
在给组件传入 prop 值的时候,如果未使用 v-bind
指令修饰 prop,则传入的 prop 值是静态的。静态 prop 有两个特点:
- 值是静态的(写死的),不会发生响应式的更新;
- 值总是 String 字符串类型;
例如下面的 title
、cmt-count
和 favor
三个 prop 值都是静态的,且 BlogArticle
组件接收到的值都是 String 类型:
html
<!-- 请注意:title、cmt-count、favor 三个 prop 的值都是 String 类型 -->
<BlogArticle title="论持久战" cmt-count="20" favor="true" />
动态 Props
相应地,还可以使用 v-bind
(缩写 :
)来绑定动态的 Props 值。动态绑定的 prop 值可以是字面量、响应式数据或表达式:
动态绑定字面量数据:
html
<!-- 这里的3个 prop 都添加了 v-bind 指令修饰,因此它们是动态 prop -->
<!-- 写在 = 后面的是 js 字面量,因此字符串需要使用引号包裹 -->
<!-- title 的值是 String 类型;cmt-count 的值是 Number 类型;favor 的值是 Boolean 类型。 -->
<!-- 除此之外,还可以通过 prop 传入 Object、Array 等任意类型的动态值 -->
<BlogArticle :title="'论持久战'" :cmt-count="20" :favor="true" />
动态绑定响应式数据:
当响应式数据变化时,会更新传入的 prop 的值:
html
<BlogArticle :title="title" :cmt-count="cmtCount" :favor="favor" />
对应的数据源为:
js
const title = ref('论持久战')
const cmtCount = ref(22)
const favor = ref(true)
请注意:如果 prop 名和数据名相同,可以简写为 :prop名
(即省略掉 = 以及右侧的数据名):
html
<BlogArticle :title :cmt-count :favor />
动态绑定表达式数据:
在 v-bind 修饰的 prop 中,还可以使用表达式动态传入 prop 的值:
html
<BlogArticle :title="blog.title" :cmt-count="blog.cmtCount" :favor="blog.favor" />
对应的数据源为:
js
const blog = ref({
title: '论持久战',
cmtCount: 34,
favor: true
})
请注意:如果您发现需要把对象的每个属性值传入 prop,并且属性名和 prop 名相同,则可以简写为没有参数的 v-bind。示例代码如下:
html
<BlogArticle v-bind="blog" />
除此之外,prop 还支持复杂的表达式绑定:
html
<!-- 根据一个更复杂表达式的值动态传入(字符串的拼接) -->
<BlogArticle :title="blog.title + 'by' + blog.author" />
单向数据流
所有的 props 都遵循着单向绑定原则:子组件的 props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
好处:这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将变得混乱而难以理解。

不要在子组件中修改 prop 的值
单向数据流意味着:子组件的 Props 值需要从父组件传入,子组件只能被动接收 Props 值,且不能主动修改父组件传入的 Props 值。否则就破坏了单向数据流的约束,且 Vue 会在控制台抛出禁止修改 Props 的警告。
vue
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
// 导入子组件
import SingleProps from './components/SingleProps.vue'
const count = ref(0)
</script>
<template>
<!-- 在父组件中,点击修改 count 值 -->
<!-- 更新后的 count 值会自动向下流向子组件 -->
<h1 @click="count++">单向数据流 --- {{ count }}</h1>
<hr />
<SingleProps :count />
</template>
vue
<!-- 子组件 -->
<script setup>
defineProps(['count'])
</script>
<template>
<h3>接收到的 count 值是:{{ count }}</h3>
<!-- 这里修改了 prop 的值,会产生警告: -->
<!-- [Vue warn] Set operation on key "count" failed: target is readonly. -->
<button @click="count++">count +1</button>
</template>
子组件想要修改 Props 的两种情况
把 prop 的值当做初始值
如果父组件传入的 prop 值,只是用来初始化子组件的局部数据,此时我们推荐大家在子组件中单独声明一个局部数据,这个局部数据以传入的 prop 值进行初始化,并在后续业务中被持续使用。而传入的 prop 仅用来初始化此局部数据。
html
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
// 导入子组件
import SingleProps from './components/SingleProps.vue'
const count = ref(0)
</script>
<template>
<h1 @click="count++">单向数据流 --- {{ count }}</h1>
<hr />
<!-- 1. 父组件传入初始 prop 值 -->
<SingleProps :initial-value="count" />
</template>
vue
<!-- 子组件 -->
<script setup>
const props = defineProps(['initialValue'])
import { ref } from 'vue'
// 1. 子组件把初始 prop 值转存到 ref 响应式数据中
const num = ref(props.initialValue || 0)
</script>
<template>
<!-- 2. 使用转存的 ref 数据,而非 prop 初始值 -->
<h3>接收到的 count 值是:{{ num }}</h3>
<button @click="num++">count +1</button>
</template>
需要对 prop 值做进一步转换
如果父组件传入的 prop 值无法直接使用,还需做进一步的转换和处理。此时不要直接对 prop 值进行转换,而是推荐使用计算属性进行转换和处理。好处是:prop 值的更新会触发计算属性的重新求值。
vue
<!-- 父组件 -->
<script setup>
import RotatePic from './components/RotatePic.vue'
import { ref } from 'vue'
// 定义响应式数据(纯数字,没有角度单位)
const rotateDeg = ref(Math.ceil(Math.random() * 23) * 15)
</script>
<template>
<button @click="rotateDeg -= 15">逆时针旋转15°</button>
<button @click="rotateDeg += 15">顺时针旋转15°</button>
<hr />
<!-- 传入响应式数据 -->
<RotatePic :deg="rotateDeg" />
</template>
vue
<!-- 子组件 -->
<script setup>
const props = defineProps(['deg'])
import logo from '../assets/logo.svg'
import { computed, watch, nextTick } from 'vue'
// 把不包含角度单位的数字,转换为包含角度单位的值
const rotateDeg = computed(() => props.deg + 'deg')
// 根据传入的 prop 值(数字),判断图片是否摆正了
watch(
() => props.deg,
(deg) => {
if (deg % 360 === 0) {
console.log('通过测试!')
}
}
)
</script>
<template>
<br />
<img :src="logo" />
</template>
<style>
img {
width: 150px;
height: 150px;
position: absolute;
left: 50%;
top: 50%;
/* 样式绑定 */
transform: translate(-50%, -50%) rotateZ(v-bind('rotateDeg'));
}
</style>
更改对象 / 数组类型的 Props
根据单向数据流的原则,子组件中不应该修改父组件传入的 Props 值。但如果父组件向下传入的是对象/数组类型的数据,此时子组件修改对象中的属性或数组中的元素,会直接影响到父组件中的数据。
因为父组件向下传递对象/数组类型的 Props 时,是把引用传入了子组件。子组件会根据引用直接修改到父组件中的数据。这会破坏单向数据流的约束,因此在实际开发中,请尽量避免这种逆向的数据更改。
vue
<!-- 子组件 -->
<script setup>
defineProps(['user', 'hobby'])
</script>
<template>
<h3>User:</h3>
<pre>{{ JSON.stringify(user, null, ' ') }}</pre>
<button @click="user.age++">age++</button>
<h3>Hobby:</h3>
<pre>{{ JSON.stringify(hobby, null, ' ') }}</pre>
<button @click="hobby.push(hobby.length)">添加元素</button>
</template>
vue
<!-- 父组件 -->
<script setup>
import ReferenceProps from './components/ReferenceProps.vue'
import { ref } from 'vue'
const user = ref({ name: 'zs', age: 20 })
const hobby = ref(['吃', '喝', '抽'])
</script>
<template>
<p>User:{{ JSON.stringify(user) }}</p>
<p>Hobby:{{ JSON.stringify(hobby) }}</p>
<hr />
<ReferenceProps :user :hobby />
</template>
最佳实践
如果子组件需要修改到父组件中的数据,子组件应该抛出一个事件*来通知父组件做出改变。
Props 校验
想象一下:在项目团队中,小红要调用别人封装好的通用组件。小红在使用别人的组件时,会有如下的 4 个疑问:
- 这个组件是否需要传入 Props,需要传入哪些 Props?
defineProps(['title', 'cmt-count'])
- 每个 prop 应该传入何种类型的数据?
- 每个 prop 都是必传项吗,如果漏传了会怎样?
- 未传/漏传的 prop 是否提供了默认值?
针对上述的第 2~4 个问题,Vue 提供了对应的解决方案,那就是 Props 校验。
基础校验
在调用 defineProps()
宏函数时,如果传入的是数组,则只是声明了 Props 的名称,并没有指定每个 prop 所要求的数据类型。
为此,Vue 提供了对象格式的入参,其中键是 prop 名,值是当前 prop 所要求的数据的类型。如果使用组件时传入的数据类型不匹配,则会导致控制台的警告:
js
// const props = defineProps(['title', 'articleType', 'cmtCount', 'favor'])
const props = defineProps({
// prop 名称: 数据类型
title: String,
articleType: String,
cmtCount: Number,
favor: Boolean
})
在这里,最常用的数据类型如下:
String、Number、Boolean、Array、Object
其它不常用的数据类型如下:
Date、Function、Symbol、Error
多种可能的类型
通过数组可以为 prop 指定多种可能的类型,例如下面的 articleType
值可以是字符串或数字:
js
const props = defineProps({
title: String,
// 这里的 articleType 值可以是 String 或 Number 类型,但不能是其它类型
articleType: [String, Number],
cmtCount: Number,
favor: Boolean
})
必传校验
默认情况下不会对未传入的 prop 进行必传校验,除非我们显示指定了 required: true
选项:
js
const props = defineProps({
// 仅校验类型
title: String,
// 多种可能的类型
articleType: [String, Number],
// 类型 + 必传
cmtCount: {
// type 用来指定类型
type: Number,
// required: true 用来指定必传项
required: true
},
favor: Boolean
})
默认值
通过 default
选项,可以为 prop 指定默认值。默认值可以提高组件的健壮性和灵活性:
js
const props = defineProps({
title: String,
articleType: [String, Number],
cmtCount: {
type: Number,
required: true,
// required 和 default 同时存在时,它们会同时生效
default: 0
},
favor: {
type: Boolean,
// default 用来指定默认值
default: false
}
})
对象类型 & 默认值
对象类型的 prop 可以使用 Object
或自定义的 class 类来指定。区别是:自定义的 class 类可以约束对象中的属性,而 Object 不能。
使用 Object 作为对象类型
js
const props = defineProps({
title: String,
articleType: [String, Number],
cmtCount: {
type: Number,
required: true,
default: 0
},
favor: {
type: Boolean,
default: false
},
// author 为 Object 类型,此时不约束对象的属性,
// 因此传入任何对象都不会产生警告
author: Object
})
使用自定义的 class 作为对象类型
定义
propTypes.js
的模块:js// 自定义的 class 类,用来约束对象类型的 prop 中的属性 export class Author { constructor(name, age) { this.name = name this.age = age } }
在自定义组件中导入并使用
Author
类,从而约束对象中的属性:js// 导入需要的 class 类 import { Author } from '../propTypes' const props = defineProps({ title: String, articleType: [String, Number], cmtCount: { type: Number, required: true, default: 0 }, favor: { type: Boolean, default: false }, // 把 class 类作为 prop 的类型, // Vue 会在内部调用 instanceof Author 来判断 prop 的值是否为 Author 类的一个实例 author: Author })
在使用自定义组件时,创建 class 类的实例之后,将实例当做 prop 值传入组件:
vue<script setup> import BlogArticle from './components/BlogArticle.vue' // 1. 导入 class 类 import { Author } from './propTypes' // 2. 创建 class 实例 const author = new Author('刘龙宾', 12) </script> <template> <!-- 3. 传入 prop 值 --> <BlogArticle :title="'论持久战'" :article-type="'军事'" :cmt-count="6" :author /> </template>
注意
自定义的 class 类不要直接放到 <script setup> 中,因为 defineProps()
宏中的参数不可以访问 <script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
对象类型的默认值
Vue 官方规定:对象或数组的默认值必须从一个 default()
工厂函数中返回,且工厂函数的形参是当前组件接收到的原始 props:
js
// 导入需要的 class 类
import { Author } from '../propTypes'
const props = defineProps({
title: String,
articleType: [String, Number],
cmtCount: {
type: Number,
required: true,
default: 0
},
favor: {
type: Boolean,
default: false
},
author: {
type: Author,
// 工厂函数,用来返回对象的默认值
default(rawProps) {
// 形参中的 rawProps 是组件接收到的原始 props 对象
console.log(rawProps)
// return { name: 'ls', age: 30 }
return new Author('佚名', 0)
}
}
})
注意
如果 type: Object
则工厂函数直接 return 字面量的对象即可; 如果 type
是自定义的 class 类,则工厂函数必须 return 一个 class 类的实例。
函数类型 & 默认值
函数类型的 prop 用 Function
来表示,函数类型的默认值就是 default()
函数(请注意,这里的 default 函数不再是工厂函数,而是默认值)。
js
// 导入需要的 class 类
import { Author } from '../propTypes'
const props = defineProps({
title: String,
articleType: [String, Number],
cmtCount: {
type: Number,
required: true,
default: 0
},
favor: {
type: Boolean,
default: false
},
author: {
type: Author,
default(rawProps) {
return new Author('佚名', 0)
}
},
readMore: {
type: Function,
// 请注意:这里的 default 函数不是工厂函数,
// 而是被当做了默认值,如果用户没有传入一个函数,
// 则把 default 函数作为 readMore 函数进行调用。
default() {
console.log('执行了默认的 default 函数!')
}
}
})
再次强调
在对象或数组类型的 prop 中,default 是工厂函数;在函数类型的 prop 中,default 是默认值。
自定义类型校验函数
如果以上的类型校验无法满足实际的校验需求,Vue 还允许程序员通过 validator()
来自定义类型校验的函数。例如:articleType 的 prop 值只能在新闻、军事、娱乐中三选一:
js
const props = defineProps({
title: String,
articleType: {
// 自定义的类型校验函数,接收两个形参:
// 形参1:用户传入的 prop 值
// 形参2:用户传入的所有的 props 对象(3.4+)
validator(value, props) {
// 如果 return 的结果为 true 表示校验通过;
// 如果 return 的结果为 false 表示校验不通过,会在控制台输出警告消息。
return ['新闻', '军事', '娱乐'].includes(value)
},
// validator() 也能配合 required 和 default 一起使用
required: true,
default: '军事'
},
cmtCount: {
type: Number,
required: true,
default: 0
},
favor: {
type: Boolean,
default: false
},
author: {
type: Author,
default(rawProps) {
console.log(rawProps)
return new Author('佚名', 0)
}
},
readMore: {
type: Function,
default() {
console.log('执行了默认的 default 函数!')
}
}
})
可为 null 的类型
如果该类型是必传但可为 null 的,你可以用一个包含 null
的数组语法:
js
const props = defineProps({
// title 必传,但可为 null
title: {
type: [String, null],
required: true
}
// ...省略其它 props
})
请注意,如果 type
仅为 null
而非使用数组语法,它将允许任何类型:
js
const props = defineProps({
// title 虽然是必传,但它允许任何类型
title: {
type: null,
required: true
}
// ...省略其它 props
})
Boolean 类型转换
对于 Boolean 类型的 prop,用户在传入 prop 值的时候完整写法如下:
html
<!-- 传入的值为 true -->
<BlogArticle :favor="true" />
<!-- 传入的值为 false -->
<BlogArticle :favor="false" />
同时 Vue 还提供了简化的写法:
html
<!-- 表示:传入的 favor 值为 true -->
<BlogArticle favor />
<!-- 表示:传入的 favor 值为 false -->
<BlogArticle />
响应式 Props 解构 3.5+
参考链接
https://blog.vuejs.org/posts/vue-3-5#reactive-props-destructure
https://vuejs.org/guide/components/props.html#reactive-props-destructure
在 Vue3.5 之前对 Props 进行解构会丢失响应性,从 Vue3.5 开始支持响应式 Props 解构。示例代码如下:
js
const { count } = defineProps(['count'])
watchEffect(() => {
// 在 3.5 之前只运行一次
// 在 3.5+ 中在 "count" prop 变化时重新执行
console.log(count)
})
当我们需要使用 watch()
侦听解构的 prop 时,需要把它包装在 getter 中:
js
watch(() => count /* ... */)
此外,当我们需要传递解构的 prop 到外部函数中并保持响应性时,也推荐把它包装在 getter 中:
js
useComposable(() => count)