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

Vue3 props怎么用更靠谱顺手?新手必懂的避坑进阶攻略

terry 55分钟前 阅读数 13 #Vue
文章标签 Vue3 props

最近帮团队带了几个刚转Vue3的前端实习生,发现他们一开始都容易把Vue2的props习惯直接套过来,要么踩了单向数据流的坑,要么浪费了Vue3新增的特性,要么写出来的代码维护起来特别麻烦,其实只要摸透Vue3 props的底层逻辑和新增用法,完全可以做到既规范又高效,今天就把我整理的实操经验和避坑点分享给大家,不管是刚入门还是想升级代码质量的开发者都能用得上。

新手入门第一步:明确Vue3 props的4个核心规则

很多人一开始用Vue3 props的时候出错,根本原因就是没搞清楚它的底层设计,Vue官方团队之所以这么设计,就是为了让数据流更清晰,组件更独立可复用。 第一个核心规则是严格的单向数据流——只能父组件通过props传值给子组件,子组件不能直接修改接收到的props,实习生最常犯的错就是直接在子组件里用v-model绑props,或者直接写this.title = '新标题'这种代码(虽然Vue3用setup不用this,但直接改ref绑定的props属性值也是不行的),如果子组件真的需要修改数据怎么办?有两种合法途径:要么给子组件传一个触发父组件更新的自定义事件,比如子组件emit('updateTitle', '新值'),父组件那边用@updateTitle="val => title = val"或者直接用v-model的语法糖;要么在子组件内部把props转成一个本地的响应式副本,比如用const localTitle = ref(props.title),不过这种方法要注意如果父组件的props值变了,localTitle会不会同步更新,需要的话可以用watch或者computed。 第二个核心规则是props声明越规范越安全——Vue3虽然允许直接用字符串数组声明props,比如props: ['title', 'count'],但强烈不推荐这么做,没有类型校验、默认值和是否必填的声明,不仅开发时IDE没法给你提示,运行时也容易出现类型错误,比如传了一个字符串给需要数字的props,导致计算逻辑全错,Vue3支持用对象声明props,可以设置type、required、default、validator这些属性,完全能满足复杂场景的需求。 第三个核心规则是props是响应式的,但不是深度响应式的?不对,是要分情况——在Vue3里,如果你给子组件传的是一个普通的引用类型(比如对象、数组)的字面量,父组件直接修改这个引用类型的内部属性,子组件的props是会自动更新的,因为Vue3的响应式系统是基于Proxy的,默认会深度监听对象和数组的变化;但如果父组件是直接替换这个引用类型的整个值,子组件的props当然也会更新,不过这里有个新手容易忽略的点:如果父组件传的是一个computed属性的结果,或者是通过props传过来再给子组件传的,响应式逻辑会不会正常?其实只要父组件那边的数据源是响应式的,子组件接收到的props就会是响应式的,不用额外做什么处理。 第四个核心规则是Boolean类型的props有特殊处理——比如父组件给子组件传了<MyButton disabled />,那子组件接收到的disabled就是true;如果父组件传了<MyButton disabled="false" />,那接收到的还是true,因为字符串“false”在Boolean类型校验下会被转换成true,如果要传false给Boolean类型的props,要么不传这个属性,要么传disabled="false",要么在props声明里给default设为true/false以外的值?不对,Boolean类型的default只能是true或者false,哦对,必须用v-bind绑定才能传动态的布尔值,包括false。

避坑进阶篇:解决新手最常遇到的6个Vue3 props问题

刚才说了核心规则,现在来聊聊实际开发中最容易踩的坑,以及怎么用Vue3的新增特性优化props的使用。

第一个坑:直接修改引用类型的props

刚才提到过直接修改props的字面量(不管是基本类型还是引用类型的替换)会报错,但直接修改引用类型的内部属性不会报错,可这依然是违反单向数据流的!为什么呢?因为如果多个子组件共用同一个父组件传下来的对象,其中一个子组件修改了内部属性,其他子组件都会受影响,数据流就乱了,调试起来特别麻烦。 那合法的处理方式有哪些?刚才说了两种,这里再补充一种——如果子组件修改的是对象的某个具体属性,父组件又不想每次都写自定义事件,可以给子组件传这个属性的单独引用,或者用Vue3新增的v-model绑定多个属性的特性,比如父组件可以写<UserForm v-model:name="user.name" v-model:age="user.age" />,子组件那边只需要分别emit('update:name', '新名字')和emit('update:age', 20)就行,比单独写多个自定义事件简洁多了。

第二个坑:设置引用类型props的default值时出错

很多新手会这么写引用类型的默认值:

props: {
  userInfo: {
    type: Object,
    default: { name: '未知', age: 18 }
  }
}

这样写看起来没问题,但实际上如果有多个地方用到这个子组件,而且都没有传userInfo这个props,那所有子组件的userInfo都会指向同一个对象!因为JavaScript里的对象是引用传递的,default如果直接写对象字面量,会在组件定义的时候就创建好这个对象,然后所有实例都共用它。 正确的写法是把default写成一个返回对象/数组的函数:

props: {
  userInfo: {
    type: Object,
    default: () => ({ name: '未知', age: 18 })
  },
  tags: {
    type: Array,
    default: () => []
  }
}

这样每个子组件实例都会有一个独立的默认对象/数组,不会互相影响。

第三个坑:不知道怎么在setup里解构props而不丢失响应式

在Vue3的setup语法糖或者setup函数里,很多人为了方便会直接解构props,

// 错误示范
const props = defineProps(['title', 'count'])
const { title, count } = props
// 这里如果父组件修改了title或者count,解构出来的title和count不会更新!

为什么会这样?因为解构出来的基本类型变量是值传递的,引用类型虽然是引用传递,但如果父组件替换了整个引用类型,解构出来的变量还是指向原来的地址。 那怎么在setup里既能解构props,又能保持响应式?Vue3提供了一个toRefs函数,专门用来解决这个问题:

// 正确示范
const props = defineProps(['title', 'count'])
const { title, count } = toRefs(props)和count都是ref,父组件修改的话会自动更新
// 注意:如果props里的某个属性是可选的,而且父组件没有传,toRefs会把它转换成一个值为undefined的ref,不会报错

不过这里有个小细节:如果props里的某个属性是引用类型,而且你只需要修改它的内部属性(虽然不推荐直接改,但有时候为了快速开发可能会这么做),直接用props.userInfo就行,不用toRefs也不用toRef,因为它本身就是响应式的。

第四个坑:不知道怎么用PropType泛型给复杂的props类型做校验

在Vue2里,给复杂的类型(比如接口类型、联合类型)做校验只能用validator函数,

// Vue2的写法
props: {
  status: {
    type: String,
    validator: (val) => ['pending', 'success', 'error'].includes(val)
  },
  userList: {
    type: Array,
    validator: (val) => val.every(item => item.id && item.name)
  }
}

这种写法虽然能满足需求,但类型不够明确,IDE没法给你很好的提示,Vue3提供了PropType泛型,可以配合TypeScript给复杂的类型做类型声明和运行时校验:

// Vue3 + TypeScript的写法
import type { PropType } from 'vue'
interface User {
  id: number
  name: string
  age?: number
}
defineProps({
  status: {
    type: String as PropType<'pending' | 'success' | 'error'>,
    required: true
  },
  userList: {
    type: Array as PropType<User[]>,
    default: () => []
  }
})

这样写不仅有运行时的类型校验(比如如果status传了'loading',控制台会报错),还有TypeScript的编译时类型检查,IDE也会给你自动补全status的可选值和userList里每个item的属性,开发效率和代码安全性都提高了很多。

第五个坑:父组件传值时用了v-bind="obj"但不知道怎么过滤不需要的属性

在Vue3里,如果你给子组件传了一个对象,而且用了v-bind="obj"的语法糖,比如<MyComponent v-bind="userInfo" />,那userInfo里的所有属性都会被当成props或者attrs传给子组件,如果子组件里已经用defineProps声明了需要的属性,那剩下的属性会被自动放到attrs里,不会报错,但有时候我们想明确告诉子组件哪些属性是必须的,哪些是可选的,或者不想让某些属性被当成attrs传给子组件的根元素,怎么办? Vue3提供了definePropsdefineAttrs两个编译器宏(不需要import就能直接用),可以配合使用来过滤不需要的属性,如果你想让某些属性即使被声明了props,也不要被当成props处理,或者想手动指定哪些属性放到attrs里,可以用inheritAttrs选项,把它设为false,然后在子组件的根元素或者其他元素上手动绑定attrs:

// Vue3 setup语法糖的写法
const props = defineProps(['title', 'count'])
const attrs = defineAttrs()
// inheritAttrs默认是true,如果要设为false,需要在script setup外面单独写
// 或者用 <script setup inherit-attrs="false"> 这种写法(Vue3.3+支持)

第六个坑:不知道怎么用Vue3.3+新增的defineProps简化写法

Vue3.3版本更新了很多实用的特性,其中就包括defineProps的简化写法,在Vue3.3之前,如果你想在setup语法糖里用TypeScript给defineProps做类型声明,有两种方式:一种是刚才说的对象声明+PropType泛型,另一种是类型参数声明,

// Vue3.3之前的类型参数声明写法
interface Props { string
  count: number
  userInfo?: User
}
const props = defineProps<Props>()
// 这种写法虽然类型更清晰,但没法直接设置默认值,需要用withDefaults编译器宏
const props = withDefaults(defineProps<Props>(), { '默认标题',
  count: 0,
  userInfo: () => ({ id: 0, name: '未知' })
})

Vue3.3之后,类型参数声明也支持直接设置默认值了,而且是更简洁的写法:

// Vue3.3+的简化写法
interface User {
  id: number
  name: string
  age?: number
}
const props = defineProps<{ string
  count: number
  userInfo?: User
}>()
// 或者单独提取接口
interface Props { string
  count: number
  userInfo?: User
}
const props = defineProps<Props>()
// 如果需要设置默认值,还是要用withDefaults,但可以用更简洁的解构赋值写法
const { title = '默认标题', count = 0, userInfo = () => ({ id: 0, name: '未知' }) } = withDefaults(defineProps<Props>(), {})
// 哦不对,刚才的解构赋值写法不太对,withDefaults返回的是一个响应式对象,不能直接解构赋值基本类型的默认值,除非用toRefs包裹:
const props = withDefaults(defineProps<Props>(), { '默认标题',
  count: 0,
  userInfo: () => ({ id: 0, name: '未知' })
})
const { title, count, userInfo } = toRefs(props)
// 这样就对了

Vue3.3还新增了defineProps的解构赋值语法糖?不对,刚才的是结合toRefs的写法,Vue3.3新增的是defineEmits的解构赋值语法糖useSlots、useAttrs的编译器宏简化写法,不过和props相关的主要还是defineProps的类型参数声明和withDefaults的配合使用更流畅了。

性能优化篇:用这2个技巧让Vue3 props的响应式更高效

刚才说了避坑和进阶用法,现在来聊聊性能优化,很多人觉得Vue3的性能已经够好了,不需要优化props,但其实在处理大量数据或者复杂组件的时候,优化props的响应式还是能带来明显的性能提升的。

第一个技巧:尽量避免传不必要的props

这一点看起来很简单,但很多人在开发的时候容易忽略,比如父组件有一个包含10个属性的对象,传给子组件的时候直接用了v-bind="obj",但子组件其实只用到了其中的2个属性,这样做不仅会让子组件的props声明变得混乱,还会让Vue3的响应式系统监听不必要的属性变化,浪费性能。 正确的做法是只传子组件需要的属性,比如<MyComponent :title="obj.title" :count="obj.count" />,或者用对象的解构赋值来过滤不需要的属性,比如<MyComponent v-bind="{ title: obj.title, count: obj.count }" />

第二个技巧:对于不需要响应式的props,可以用shallowRef或者shallowReactive包裹

在Vue3里,默认的响应式系统是深度监听的,也就是不管你传的对象有多少层嵌套,Vue都会把它转换成响应式的,但如果你传的props是一个很大的静态数据(比如一个下拉框的选项列表,不会变化),或者是一个不需要深度监听的对象(比如只需要监听整个对象的替换,不需要监听内部属性的变化),深度监听就会浪费性能。 这时候可以用shallowRef或者shallowReactive包裹父组件里的数据源,然后传给子组件,shallowRef只会监听整个值的替换,不会监听内部属性的变化;shallowReactive只会监听对象第一层属性的变化,不会监听第二层及以上的属性变化。 不过这里有个小细节:如果用shallowRef或者shallowReactive包裹数据源,父组件直接修改内部属性的话,子组件的props不会更新,只有替换整个值的时候才会更新,这一点要注意和业务需求匹配。

Vue3 props是父子组件通信的核心方式之一,只要摸透了它的4个核心规则,避开了常见的6个坑,再用上2个性能优化技巧,就能写出既规范又高效的代码,Vue3的版本更新很快,尤其是3.3之后新增了很多实用的特性,建议大家多关注官方文档,及时学习新特性,提升自己的开发效率。

今天的分享就到这里,如果你有其他关于Vue3 props的问题,或者想了解更多Vue3的实用技巧,欢迎在评论区留言讨论。

版权声明

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

热门