Vue3 defineProps 到底怎么用?避坑指南、类型写法全解析
你刚接触Vue3写组件,有没有被defineProps搞晕过?一会儿要用数组一会儿要用对象,还有TypeScript专属的写法,有的时候传参明明正确控制台还报错,改半天不知道问题出在哪?这篇文章就从0到1拆解defineProps的用法,从基础写法到TS进阶,再到你大概率踩过的坑,全给你讲明白,看完就能直接上手写规范的props定义。
defineProps 是个啥?它解决了Vue2的什么问题?
先搞懂这个基础概念,才能明白为什么Vue3推荐用它,在Vue2里,我们一般是在组件的props选项里定义接收参数,然后在setup()函数内部通过props参数访问,但Vue3的script setup语法糖简化了组件结构,原来的props选项和setup里的props参数怎么对应呢?defineProps就是这个桥梁——它是script setup专属的编译宏,不用引入就能用,运行时会被Vue自动转换。
那它比Vue2的props好在哪儿?除了和script setup完美适配,最大的亮点是更灵活的类型支持——特别是配合TypeScript的时候,既能用JS的对象类型定义,也能用TS的泛型直接写接口,不用重复声明类型,减少出错的概率,还有,defineProps是编译时处理的,不会有运行时的额外开销,组件加载速度更快。
基础JS写法:数组和对象各有什么适用场景?
如果你的项目暂时不用TypeScript,或者组件很简单,那用基础的JS写法就够了,defineProps有两种基础写法:数组式和对象式。
数组式:最无脑的快速写法
数组式就是把接收的参数名用字符串列成一个数组,直接传给defineProps,
<script setup> const props = defineProps(['title', 'content', 'count']) </script>
这种写法适合什么场景?完全不需要验证参数的情况,比如父子组件之间传的是非常确定的基础值,而且不会传错类型、不会漏传,举个例子,一个简单的“评论数量显示组件”,只需要接收父组件传的count,那用数组式就没问题,写起来快,不啰嗦。
但这里要提一句:尽量少用数组式,除非组件真的临时用、或者完全确定不会有人改,因为没有参数验证,万一父组件传了个对象进来显示评论数量,控制台不会报错,但页面会显示[object Object],调试起来特别麻烦。
对象式:有验证才靠谱的生产写法
生产环境90%以上的情况都要用对象式写法,因为它能给每个参数加类型限制、必填校验、默认值、自定义校验函数,能提前把错误扼杀在开发阶段,不用等页面出问题再找原因。
对象式的基本结构
<script setup>
const props = defineProps({
// 参数名作为键,值可以是类型构造函数(String/Number/Boolean/Array/Object/Date/Function/Symbol),也可以是更详细的验证对象 String, // 只限制类型
content: { // 详细验证对象
type: String,
required: true, // 必填,不传的话开发环境会直接在控制台红色警告
},
count: {
type: Number,
default: 0, // 非必填,不传的话用默认值
// 自定义校验函数,接收传进来的参数值,返回true/false
validator: (value) => {
return value >= 0
}
},
tags: {
type: Array,
// 注意!对象和数组的默认值不能直接写{}或[],必须用函数返回!
default: () => ['Vue', 'React']
},
userInfo: {
type: Object,
// 对象也是一样,用函数返回默认值
default: () => ({
name: '游客',
age: 0
})
}
})
</script>
对象式里的几个关键坑
刚才的代码里其实已经提到了两个高频踩坑点,这里单独拎出来说,别再犯了:
- 对象/数组的默认值必须用函数返回:这是因为JS里对象和数组是引用类型,如果直接写{}或[]作为默认值,所有复用这个组件的实例会共享同一个对象/数组,改了一个组件的tags,其他所有组件的tags都会跟着变,这绝对是个大bug,用函数返回的话,每个实例都会生成一个新的引用类型,互不影响。
- Boolean类型的特殊处理:Vue3和Vue2在Boolean的默认值上是一致的,但新手还是容易搞错,如果一个参数的type是Boolean,而且没有required和default,那它的默认值是false;如果有其他类型,但默认值是false,那传值的时候要小心——比如一个组件的isShow是Boolean,默认值false,如果父组件只写
<MyComponent is-show>,那isShow会被自动设为true,不用写is-show="true"。
TypeScript专属写法:泛型接口一步到位
如果你用了TypeScript,那强烈推荐用泛型接口+as const断言(或者直接用字面量类型注解)的写法,类型提示更精准,还不用像JS对象式那样重复写类型信息,编译时就能检查类型错误,连运行时的警告都能提前避免。
用interface定义Props接口
这是最常用的TS写法,先定义一个单独的Props接口,再把它传给defineProps的泛型:
<script setup lang="ts">
// 先定义Props接口,和普通的TS接口一样
interface Props {: string // 问号表示可选,非必填
content: string // 没有问号表示必填
count: number
tags?: string[]
userInfo?: {
name: string
age: number
}
}
// 直接把Props作为泛型传给defineProps
const props = defineProps<Props>()
</script>
这种写法的好处是:接口可以单独复用,比如两个组件接收的参数差不多,稍微改改就能用;类型提示非常完整,在模板里写{{ props. }}的时候,编辑器会自动弹出可选的属性,还会提示每个属性的类型和是否必填。
但这里有个问题:TS泛型写法不能直接在接口里写默认值,那怎么办?别慌,Vue3提供了withDefaults这个编译宏来解决。
配合withDefaults设置默认值
withDefaults的第一个参数是defineProps的泛型调用,第二个参数是默认值的对象,写法如下:
<script setup lang="ts">
interface Props {: string
content: string
count: number
tags?: string[]
userInfo?: {
name: string
age: number
}
isDark?: boolean
}
// withDefaults设置默认值
const props = withDefaults(defineProps<Props>(), { '默认标题',
count: 0,
tags: () => ['Vue', 'TypeScript'], // 同样,数组/对象用函数返回
userInfo: () => ({ name: '默认用户', age: 18 }),
isDark: false
})
</script>
withDefaults的默认值对象会自动和Props接口匹配,如果接口里没有对应的属性,编辑器会直接报错,不会出现JS里拼写错误默认值的情况。
直接用字面量类型注解
如果接口很简单,不想单独定义,可以直接把字面量类型写在泛型里:
<script setup lang="ts">
const props = defineProps<{: string
content: string
count?: number
}>()
</script>
这种写法适合组件参数特别少的情况,比如只有2-3个参数,写起来比定义接口更简洁,但如果参数多,或者有嵌套对象,还是建议用interface,可读性更好。
defineProps还有这些冷知识,你可能不知道
除了基础和TS写法,defineProps还有几个不太常用但关键时刻能救命的小技巧。
用v-bind绑定所有props
如果父组件有一个对象,里面的所有键值对都要传给子组件,不用一个个写title="obj.title" :content="obj.content",直接用v-bind="obj"就行,
<!-- 父组件 -->
<script setup>
import MyComponent from './MyComponent.vue'
const post = { 'Vue3 defineProps全解析',
content: '这是文章内容',
count: 100,
tags: ['Vue', 'TypeScript']
}
</script>
<template>
<MyComponent v-bind="post" />
</template>
<!-- 子组件MyComponent.vue -->
<script setup>
const props = defineProps(['title', 'content', 'count', 'tags'])
</script>
这个技巧能节省很多代码,特别是当参数很多的时候,但要注意:v-bind绑定的对象里的键必须和子组件定义的props完全一致,否则会被当成普通的HTML属性挂在子组件的根元素上(除非子组件设置了inheritAttrs: false)。
解构props但保留响应式
很多新手会直接解构props,
<script setup>
const props = defineProps(['count'])
const { count } = props
</script>
这样写的话,count会失去响应式——父组件传的count变了,子组件里的解构变量count不会更新,那怎么保留响应式呢?有两种方法:
用toRefs
toRefs可以把一个响应式对象(props是响应式的)的所有属性都转换成ref,解构之后的属性还是响应式的:
<script setup>
import { toRefs } from 'vue'
const props = defineProps(['count', 'title'])
const { count, title } = toRefs(props)
// 解构之后的count和title是ref,访问的时候要加.value(在模板里不用)
console.log(count.value)
</script>
用toRef单独转换某个属性
如果只需要解构一个属性,用toRef更轻量:
<script setup>
import { toRef } from 'vue'
const props = defineProps(['count'])
const count = toRef(props, 'count')
console.log(count.value)
</script>
这两种方法都能保留响应式,具体用哪种看你的需求。
设置默认属性名
父组件传的参数名如果和子组件的不一样怎么办?比如父组件习惯用article-title,子组件习惯用title,这时候可以用对象式写法里的prop键(注意!Vue2里是model或者直接用kebab-case转camelCase,但如果想明确指定的话Vue3保留了prop?不对等下,确认一下:哦对,Vue3里还是和Vue2一样,默认是自动转kebab-case和camelCase的,但如果想强行指定,比如子组件想用articleTitle但父组件必须传art-title(虽然没必要但可以),不过刚才差点写错了,自动转换是基础,比如父组件写<MyComponent art-title="xxx" />,子组件定义articleTitle: String就能直接访问props.articleTitle,不用额外设置,那刚才的冷知识应该换个?比如defineProps返回的对象是只读的!对,这个更重要,很多新手会直接修改props,
<script setup> const props = defineProps(['count']) props.count++ // 这里会报错! </script>
Vue3的props是严格只读的,不管是在JS里还是在模板里,都不能直接修改,否则开发环境会红色警告,那如果子组件想修改props怎么办?按照Vue的单向数据流原则,应该让子组件触发一个事件,父组件监听这个事件然后修改自己的状态,再把新的状态传给子组件。
最后再给你总结几个必记的避坑点
- 数组式写法尽量少用,除非完全确定参数没问题,生产环境用对象式或TS泛型式。
- 对象/数组的默认值必须用函数返回,不管是JS写法还是TS的withDefaults写法。
- TS泛型写法配合withDefaults设置默认值,类型提示更精准,还不会拼错属性名。
- 解构props要配合toRefs或toRef,否则会失去响应式。
- props是严格只读的,不能直接修改,要通过触发事件让父组件修改。
defineProps其实不难,只要搞懂它的编译本质、基础写法、TS进阶写法,再记住几个必记的避坑点,写组件的时候就不会再踩雷了,你现在可以打开你的Vue3项目,试试把之前写的组件的props改成更规范的写法,看看有没有之前没发现的错误。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

