Code前端首页关于Code前端联系我们

Vue3 withDefaults是做什么的?怎么用它写更安全的Props默认值?

terry 3小时前 阅读数 50 #Vue

为什么Vue3新增了withDefaults?之前的默认值写法有啥问题?

刚接触Vue3组合式API的同学,可能一开始还在用Options API那套props默认值写法——在defineProps里给属性加default属性对吧?比如定义一个按钮组件的props:

// 不带TS的旧组合式/Options通用写法思路
const props = defineProps({
  type: {
    type: String,
    default: 'primary'
  },
  size: {
    type: String,
    default: 'medium'
  },
  loading: {
    type: Boolean,
    default: false
  },
  config: {
    type: Object,
    default: () => ({ shadow: true, rounded: 'sm' })
  }
})

这套写法在纯JavaScript环境下没啥大问题,但Vue3官方推荐搭配TypeScript,组合式API+defineProps泛型的写法现在更是主流,这时候问题就来了:用泛型写defineProps的时候,属性的类型能明确,但默认值和类型怎么关联起来?

比如很多新手刚开始会写成这样:

// ❌ 错误示范1:泛型里写TS类型,defineProps外面单独赋值默认值
// 组件接收不到这些赋值,因为defineProps返回的是只读的props对象!
const props = defineProps<{
  type?: 'primary' | 'secondary' | 'danger'
  size?: 'small' | 'medium' | 'large'
  loading?: boolean
  config?: { shadow: boolean; rounded: 'none' | 'sm' | 'md' | 'lg' }
}>()
// 下面这行修改只读对象的代码会报错
props.type = 'primary' 

还有人会用Vue2遗留的类型+默认值混合写法,但泛型用了一半又回Options风格,代码显得很乱,TS的类型推断也会受影响:

// ❌ 半吊子写法:风格割裂,TS推断不够顺畅
const props = defineProps({
  type: {
    type: String as PropType<'primary' | 'secondary' | 'danger'>,
    default: 'primary'
  },
  // 其他属性还要重复写泛型的PropType
})

甚至还有同学直接放弃用泛型,纯靠PropType写类型,这完全浪费了Vue3组合式API+TS泛型的简洁优势,维护起来也麻烦。

这时候Vue3.2版本新增的withDefaults就派上用场了——它专门用来解决泛型定义defineProps时,如何安全、规范地绑定Props默认值,同时让TS类型自动完善的问题

withDefaults到底是个什么函数?原理是什么?

先给个简单定义:withDefaults是Vue3提供的一个编译时辅助函数,注意哦,是“编译时”不是“运行时”——这意味着它不会给你打包后的代码增加任何额外的运行时开销,只是在Vue的单文件组件(SFC)或者<script setup>的编译阶段,帮你把泛型里的可选属性、默认值和TS类型自动关联,生成和之前Options风格一致的安全运行时代码。

那它的底层原理大概是啥呢?虽然咱们不用完全抠源码细节,但知道一点能更好地理解它的用法:

  1. 当编译器遇到<script setup>里的withDefaults包裹的defineProps泛型时,会先解析泛型里的类型结构,把所有带的可选属性标记出来;
  2. 然后读取你传给withDefaults的第二个参数(默认值对象),检查默认值的类型是否和泛型里对应属性的类型匹配——不匹配的话TS会在编译前就给你报错,提前规避类型问题;
  3. 编译器会把这些默认值“注入”到生成的Options风格的props定义里,同时帮你把泛型里那些有默认值的可选属性的类型,从“原类型 | undefined”优化成“原类型”——也就是说,你在组件内部用这些有默认值的属性时,不用再写非空断言()或者做undefined判断了,TS会自动认为它们一定有值!

基础用法:如何用withDefaults绑定简单类型和引用类型的默认值?

基础用法其实很简单,就是用withDefaultsdefineProps泛型包起来,第一个参数是带类型的defineProps,第二个参数是专门放默认值的普通对象,我们先拿刚才的按钮组件来改个正确的基础版本:

简单类型的默认值

// ✅ 正确基础用法:简单类型默认值
<script setup lang="ts">
// 引入withDefaults?不用!<script setup>里会自动导入编译时辅助函数
const props = withDefaults(
  defineProps<{
    type?: 'primary' | 'secondary' | 'danger' | 'warning'
    size?: 'small' | 'medium' | 'large'
    loading?: boolean
    text?: string
  }>(),
  {
    // 所有默认值直接写在这里,类型会自动对应泛型检查
    type: 'primary',
    size: 'medium',
    loading: false,
    // text属性没写默认值,所以泛型里的?不能丢,用的时候可能是undefined
  }
)
// 这里直接用props.type,TS会自动推断成'primary'|'secondary'|'danger'|'warning',没有undefined
console.log(props.type.toUpperCase()) // 不会报错!
// 这里用props.text,必须做undefined判断或者加非空断言
if (props.text) {
  console.log(props.text.length)
}
</script>

注意看代码里的注释:

  • <script setup lang="ts">里不用手动import { withDefaults } from 'vue',Vue的编译器会自动处理这些编译时工具;
  • 有默认值的属性,在泛型里可以带也可以不带?等等,等下仔细说——其实最好带,因为不带的话,父组件不传的话虽然运行时会用默认值,但TS会把它当成“必传属性”,父组件不传会报TS警告;但带了,父组件不传是允许的,TS也不会警告,同时组件内部因为有默认值,类型会自动去掉undefined,这个细节很多新手踩坑,一定要记牢!

引用类型的默认值

这个很重要!和Options API一样,引用类型(Object、Array、Function等)的默认值不能直接写对象/数组字面量,必须写一个返回该字面量的工厂函数——因为如果直接写字面量,所有复用这个组件的实例会共享同一个引用对象,一个实例修改了这个对象的属性,其他所有实例的该属性都会跟着变,这是Vue2/Vue3都必须遵守的规则!

那withDefaults里怎么写工厂函数呢?直接写在默认值对象里就行,和Options API完全一致:

// ✅ 正确基础用法:引用类型默认值用工厂函数
<script setup lang="ts">
interface ButtonConfig {
  shadow: boolean
  rounded: 'none' | 'sm' | 'md' | 'lg'
  borderWidth: number
}
const props = withDefaults(
  defineProps<{
    type?: 'primary' | 'secondary'
    config?: ButtonConfig
    onClick?: (e: MouseEvent) => void
    // 数组类型也一样
    disabledClasses?: string[]
  }>(),
  {
    type: 'primary',
    // 引用类型必须用工厂函数!
    config: () => ({
      shadow: true,
      rounded: 'sm',
      borderWidth: 1
    }),
    disabledClasses: () => [],
    // 函数类型?直接写函数也行,但工厂函数也没问题,推荐直接写更直观
    onClick: (e) => console.log('按钮点击了:', e.target)
  }
)
// 测试引用隔离:在组件内部修改props.config.shadow(当然不推荐直接修改props!这里只是演示隔离)
// 不过不管推不推荐,Vue都会确保每个实例的config是独立的
// 实际开发中应该用props触发emit,让父组件修改状态
</script>

这里顺便再提一下Props的单向数据流原则:不管有没有withDefaults,组件内部都不能直接修改props的任何属性,引用类型的属性也不能直接改它的内部值(虽然JavaScript允许,但Vue会在开发模式下给你警告,生产模式下可能出问题),必须通过defineEmits触发事件,让父组件或者根组件修改传给子组件的数据源。

进阶用法:withDefaults还有哪些实用的细节?

刚才讲的都是基础,其实withDefaults还有几个进阶细节,熟练掌握能让你的代码更安全、更简洁:

细节1:部分必填、部分可选属性的混合写法

不是所有Props都要有默认值的,比如按钮的内容(text或者slot之外的label)可能是必填的,这时候在泛型里,必填属性不带,可选属性带,默认值对象里只写可选属性的默认值就行:

// ✅ 进阶用法1:必填+可选属性混合
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    // 必传属性:不带?,默认值对象里也不能写!写了TS会报错
    label: string
    // 可选属性:带?,默认值对象里写
    type?: 'primary' | 'secondary'
  }>(),
  {
    type: 'primary'
  }
)
</script>

细节2:默认值可以是组件内部的常量吗?

当然可以!但要注意这个常量必须在withDefaults之前定义,因为withDefaults是编译时处理的,常量的定义顺序会影响编译时的解析:

// ✅ 进阶用法2:用组件内部常量当默认值
<script setup lang="ts">
// 必须放在withDefaults前面!
const DEFAULT_BUTTON_TYPE = 'primary'
const DEFAULT_BUTTON_SIZE = 'medium'
const props = withDefaults(
  defineProps<{
    type?: string
    size?: string
  }>(),
  {
    type: DEFAULT_BUTTON_TYPE,
    size: DEFAULT_BUTTON_SIZE
  }
)
</script>

如果反过来,把常量放在withDefaults后面,编译时会报错找不到该常量——这个也是很多新手容易忽略的点。

细节3:withDefaults对解构赋值的优化

很多同学喜欢在组件内部解构props,比如const { type, size } = props,但如果是纯泛型defineProps(没有withDefaults),解构出来的可选属性还是会带undefined类型,必须做判断;但用了withDefaults之后,解构出来的有默认值的属性,类型会自动去掉undefined!

// ✅ 进阶用法3:解构赋值自动优化类型
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    type?: 'primary' | 'secondary'
    size?: 'small' | 'medium'
  }>(),
  { type: 'primary', size: 'medium' }
)
// 解构出来的type和size,类型都是确定的联合类型,没有undefined
const { type, size } = props
console.log(type.toUpperCase(), size.toUpperCase()) // 完全没问题
</script>

这个优化太实用了,不用再写一堆非空断言了,代码看起来清爽很多。

细节4:如何处理复杂的联合类型默认值?

比如有个组件的data属性,既可以是字符串数组,也可以是对象数组(对象有id和label),这时候默认值怎么写?TS会不会报错? 其实只要默认值的类型符合联合类型的其中一个分支就行,withDefaults会自动处理:

// ✅ 进阶用法4:复杂联合类型的默认值
<script setup lang="ts">
interface Option {
  id: string | number
  label: string
}
const props = withDefaults(
  defineProps<{
    data?: string[] | Option[]
  }>(),
  {
    // 选联合类型的其中一个分支当默认值就行,比如空字符串数组
    data: () => []
  }
)
</script>

如果父组件不传data,那props.data就是空字符串数组,类型会被推断成string[];如果父组件传了对象数组,类型会自动变成Option[]——Vue的TS类型推断还是很智能的。

实际开发中的最佳实践

讲完了用法,再结合我自己的开发经验,说几个最佳实践:

  1. 能用<script setup lang="ts">就用,搭配withDefaults和泛型defineProps,代码最简洁,类型最安全;
  2. 所有Props都尽量用TS类型定义,不管是简单类型还是引用类型,引用类型最好单独写interface或者type,方便复用和维护;
  3. 有默认值的可选属性一定要带,避免父组件不传时TS报警告;
  4. 引用类型的默认值必须用工厂函数,这个是底线,不能破;
  5. 不要在withDefaults的默认值对象里写必传属性的默认值,TS会直接报错,父组件也必须传必传属性;
  6. 组件内部不要直接修改props,哪怕是引用类型的内部值,一定要通过defineEmits触发事件;
  7. 如果默认值比较复杂,可以单独抽成一个常量文件,比如src/constants/button.ts,然后在组件里导入(注意导入的常量也要放在withDefaults前面)。

简单回顾一下,withDefaults是Vue3.2新增的编译时辅助函数,专门用来解决泛型defineProps绑定默认值的问题:

  • 它不会增加运行时开销;
  • 它会自动检查默认值的类型是否和泛型匹配;
  • 它会自动把有默认值的可选属性的类型从“原类型 | undefined”优化成“原类型”;
  • 引用类型的默认值必须用工厂函数;
  • <script setup lang="ts">里不用手动导入。

现在再用回之前的错误示范1,对比一下,是不是withDefaults的写法简洁太多、安全太多了?如果你还在用旧的半吊子写法或者纯PropType写法,赶紧换成withDefaults+泛型defineProps吧,这是Vue3官方推荐的标准写法,也是现在前端面试Vue3时经常会问到的考点哦!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门