查看原文
其他

全面了解Vue3的组件的Props

自然框架 脚本之家 2023-07-05
将 脚本之家 设为“星标
第一时间收到文章更新

作者 | 自然框架
出品 | 脚本之家(ID:jb51net)

props 的本质

props 的本质是什么?这个问题大家似乎没有关注,其实这是一个关键点,了解之后就会明白与props相关的各种问题,比如解构后为啥没有响应性了。

官网上的介绍比较分散,这里汇总一下,尽量说全,包括Vue3.3的新特性。

官网相关文档:

  • props基础
  • v-model
  • TypeScript 与组合式 API
  • TypeScript 与选项式 API
  • Prop的泛型
  • Prop的校验
  • 标注类型
  • 访问Props
  • shallowReadonly()
  • reactive()

shallowReadonly + reactive = props

props是什么样子的呢?我们写个代码做一下对比就知道了:

import { reactive, shallowReadonly } from 'vue'

const ret = reactive({
name: 'reactive'
})
const sr = shallowReadonly(ret)

const props = defineProps({
name: String
})

console.log(sr, props)

先定义一个 reactive,然后套上shallowReadonly;再定义一个 props,打印结构对比一下,看看效果:

200props的本质.jpg

二者的结构完全一致,Proxy 的 set 拦截的代码位置一致,所以说props实质是:(composition API环境下)

  • 外壳是一个 shallowReadonly
  • 里面是一个 reactive。

reactive 都很熟悉了,那么 shallowReadonly 是什么呢?我们来看看官网:

https://cn.vuejs.org/api/reactivity-advanced.html#shallowreadonly

和 readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
谨慎使用
浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。

这个谨慎使用是官方吐槽吗?既然知道响应行为会不一致,那么为啥还要用在props上呢?

props的定义与验证

了解了本质之后,我们还是回来看看如何定义组件的 props。
这个有点像茴香豆的茴有几种写法,虽然定义方式比较多,但是我们掌握一种即可。个人推荐TS的方式

option 风格

一开始 Vue 只有 Option API,所以 props 都是用 Option 的方式定义的。

  • 定义一个简单的props
export default {
props: ['foo'],
created() {
// props 会暴露到 `this` 上
console.log(this.foo)
}
}
  • 约束类型
export default {
props: {
title: String,
likes: Number
}
}

这里的 String、Number 表示 Vue 的 props 的属性的类型,并不是 TS 的类型。
不需要安装TS,vue可以自行识别。

setup 风格

后来有了 composition API,于是可以有新的定义方式。具体又可以分为两种方式:

option + setup 方式:props 作为 setup 函数的参数传入

<script >
export default {
props: ['foo'],
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}
<script setup>

script setup 方式:使用 defineProps 编译器宏定义

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

defineProps 编译后会变成类似 setup 函数的方式

所以说,第二种方式可以看做是第一种方式的语法糖。

TS方式

为了更好的支持TS,于是有了TS风格的定义方式。
一开始可能是忙不过来,仅仅支持本地的类型,等到Vue3.3 才支持从外部导入类型。

本地定义类型

const props = defineProps<{
foo: string
bar?: number
}>()

从外部导入类型

vue 3.3 之前,props 的类型定义必须在组件内部,不支持从外部文件导入,到了Vue3.3才支持

先做一个ts文件:(base.ts)

export interface IFromItemProps {
/**
* 表单的值
*/

model: {[key: string]: any},
/**
* 对应的字段名称
*/

colName: string,
/**
* 控件的备选项,单选、多选、等控件需要
*/

optionList?: Array<{
label: string,
value: string | number | boolean,
disabled: boolean
}>,
/**
* 是否显示可清空的按钮,默认显示
*/

clearable?: boolean,
/**
* 浮动的提示信息,部分控件支持
*/

title?: string,
/**
* 组件尺寸
*/

size?: string
}

然后在组件里引入这个文件:

<script setup lang="ts">
// 引入外部的类型定义
import type { IFromItemProps } from './base'
// 使用外部的TS类型,定义一个 props ,可以增加新属性。
const props = defineProps<IFromItemProps & {multiple?: boolean}>()

</script>

这样做有几个优点:

  • 组件内部代码更简洁,可以不用看prop的类型细节。
  • 可以复用类型定义。

在 template 里面使用:

<template>
<el-select
v-model="model[colName]"
v-bind="$attrs"
:id="'c' + colName"
:name="'c' + colName"
:size="size"
:clearable="clearable"
:multiple="multiple"
>

<el-option
v-for="item in optionList"
:key="'select' + item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
>

</el-option>
</el-select>
</template>

支持泛型的 props

vue3.3 令 props 也可以支持泛型了。

<script setup lang="ts" generic="T extends Object ">
const props = defineProps<{
list: T[], // 泛型的方式
list2: number[], // 只能是 number 类型的数组
list3: Array<any> // 任意类型的数组
}>( )

这样可以更准确的进行类型推断。

运行时验证

Vue一开始的思路是,可以在运行时对 props 进行验证,可以验证属性名称、属性类型、是否必传、自定义验证函数等,还可以设置默认值。

于是在定义 props 的时候可以进行如下设定:

// export default { props: { // option API
defineProps({ // composition API
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})

以上这些设置,可以实现在运行时进行验证,如果有不符合的情况,可以通过 F12 的 Console 查看。

三种方式都支持运行时验证,只是TS的方式,目前为止似乎不支持验证函数。

编写时提示和验证

那么在编写代码的时候,是否可以有更好的提示和验证呢?这个就需要引入TS。

类型和必填

defineProps<{ msg: string }>
// 会被编译为 
{ msg: { type: String, required: true }}

defineProps<{ msg?: string }>
// 会被编译为 
{ msg: { type: String, required: false }}

// 多种类可能
defineProps<{ foo: string | number}>

这样实现了类型和必填的验证。

默认值

使用 withDefaults(编译器宏) 实现 ts方式的默认值。

export interface Props {
msg?: string
labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})

验证函数怎么办?

目前还没有发现在ts方式下如何实现 props 的验证(validator)。

v-model

v-model 是 props 的一个特殊属性,和emit配合使用,可以实现在子组件改父组件的变量。

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/
>
</template>

Vue3.3提供了一个语法糖(defineModel),可以简化上面的代码:

<script setup>
const modelValue = defineModel()
modelValue.value++
</script>

需要手动设置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
script: {
defineModel: true
}
})],
})

传入与解构

父组件赋值,子组件获取,这个大家都会,也都知道 props 解构后会失去响应性,那么大家有没有想过内部原理?

传入

父组件定义 ref、reactive 分别传入。

const name = ref('我是 ref')
const person = reactive({
name: '我是 reactive',
age: 1
})

子组件定义props

const props = defineProps<{
name: string,
person: {
name: string
}
}>()

那么子组件接收的到底是什么?

205传入.jpg

如果是ref,那么只能接收到value的值,而不是ref本身。
如果是reactive,那么可以接到reactive本身。

这个差异导致了props解构后,是否可以保持响应性。

如果还是不清楚的话,类似下面的代码:

// 父组件
const name = ref('我是 ref')
// 子组件的 props
const ret = reactive({name: name.value})
// 修改 ret的name属性,是无法改变 ref 的 value 值的。

// 通过 emit 改变的思路。仅思路,不是vue内部的实现代码,实际上的代码比较复杂。
const myEmit = (val) => {
name.value = val
}

解构

看到上面的例子,大家都会注意到一个细节,解构后无法保持响应性,其实只是对于 ref,而对于 reactive 其实还是可以保持响应性的。

那么官网为啥不详细说明呢?恐怕是怕造成心智负担吧,说多了会懵。

另一个原因可能是,大家都喜欢“单层”的props,而不是“多层”的props吧。

不过我觉得,如果想保持响应性,那么直接传入 reactive 是最简单粗暴、无副作用的方法。

使用 shallowReadonly 的原因

说了这么多,终于回到了这个问题。

原因也很简单, props 的第一层属性并不是父组件的 ref,所以直接修改无法实现响应性,所以需要使用 shallowReadonly 限制修改第一层属性。

那么为啥不直接使用 Readonly?那是因为props 可以是父组件的 reactive,这样解构出来,相当于获得了父组件定义的reactive。

如果 props 直接使用 readonly,那么就堵死了传入 reactive,实现响应性的方式。

既然留了这个“缺口”,为啥不使用一下?

还记得上面那个引入外部ts类型定义props的代码吗?其中的 model 就是一个对象,期待传入 reactive,然后在template 里面直接设置到 v-model。
<el-select v-model="model[colName]" ... >
这样是不是非常简洁了?

本文作者:自然框架

个人网址:jyk.cnblogs.com

声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。


写的不错?赞赏一下


长按扫码赞赏我

    推荐阅读:原创推荐:

【人人都可低代码】Vue3 把 el-form 变成LowCode风格的表单控件

结合 Vuex 和 Pinia 做一个适合自己的状态管理nf-state

一篇文章说清 webpack、vite、vue-cli、create-vue 的区别

【摸鱼神器】vue + 路由 + 菜单 + tabs 一次搞定之管理后台

【摸鱼神器】UI库秒变LowCode工具——列表篇(一)

20多个好用的 Vue 组件库,请查收!

基于Vue3 写一个更好用的在线帮助文档工具

通过UI库深入了解Vue的插槽的使用技巧(片尾有彩蛋)

【摸鱼神器】拖拖点点,列表现——做列表居然可以不用写代码!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存