vue3.3 新特性详解

Vue.js3.3版本带来了诸多改进,包括defineProps支持导入类型、defineEmits的简化写法、泛型组件的支持、实验性的defineModel宏、defineOptions用于定义组件选项,以及对defineSlots和toRef/toValue的增强。这些更新旨在提高开发效率和代码可读性。

本文将详细介绍 vue.js 3.3 版本的更新内容,最新版本主要目标是优化开发者的使用体验,包括引入一些新的简化语法和宏,以及在 TypeScript 方面的进一步提升。

前言

当升级到3.3时,建议同时更新以下依赖项:

  • volar / vue-tsc@^1.6.4
  • vite@^4.3.5
  • @vitejs/plugin-vue@^4.2.0
  • vue-loader@^17.1.0 (if using webpack or vue-cli)

defineProps 宏支持引入类型

defineProps 支持使用 import 从外部导入的类型声明,也支持全局变量引入。

// hi.ts
export interface HI {
  name: string;
}

// global.d.ts
declare global {
  interface HI {
    name: string;
  }
}

<script setup lang="ts">
import type { HI } from './hi'
// 支持使用导入的类型 + 交集类型(导入类型基础上增加一个字段)
defineProps<HI & { age: number }>()
</script>

defineEmits 宏更简便的写法

之前 defineEmits 的类型参数只支持调用签名语法。

const emit = defineEmits<{
  (e: 'foo', id: number): void
  (e: 'bar', name: string, ...rest: any[]): void
}>()

// 或者不定义类型
const emit = defineEmits(['update:modelValue'])

vue3.3 中可以简化为以下写法,更加简洁(当然原来的写法照样可以继续使用)。

const emit = defineEmits<{
  foo: [id: number]
  bar: [name: string, ...rest: any[]]
}>()

generic 泛型组件支持

使用 <script setup> 的组件现在可以通过 generic 属性接受泛型类型参数,也可以使用多个参数,extend 约束、默认类型和引用导入的类型。

// generic.vue
<script setup lang="ts" generic="T extends number, U extends HI">
import type { HI } from './hi'
defineProps<{ age: T[], names: U[] }>()
</script>

// 在组件中定义并使用
<generic :age="[20, 20]" :names="[{ name: '张三' }, { name: '李四' }]" />

generic 语法同样也支持在 .tsx 结尾的文件中使用:

// generic.tsx
import { defineComponent } from 'vue'
import type { HI } from './hi'

export default defineComponent(<T extends number, U extends HI>(props: { age: T[], names: U[] }) => {
  return () => <div>{props.names}</div>
})

// 在组件中定义并使用
<generic :age="[20, 20]" :names="[{ name: '张三' }, { name: '李四' }]" />

defineModel

由于在 vue3.3 中此功能是实验性的,当使用该新特性时需要进行以下配置(需要重启)。

// vite.config.ts
export default defineConfig({
  plugins: [
    vue({
      script: {
        defineModel: true
      }
    })
  ]
})

简化前自定义 v-model 双向绑定语法,需要声明 props,并定义 update:propName 事件

<template>
  <input :value="modelValue" @input="onInput" />
</template>

<script setup lang="ts">
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function onInput(e) {
  emit('update:modelValue', e.target.value)
}
</script>

以下是简化后的写法,宏将自动注册 props 和事件 ,并返回一个 ref

<template>
   <input v-model="modelValue">
</template>

<script setup lang="ts">
const modelValue = defineModel()
// 也可以直接修改,等价于emit('update:modelValue', '新的值')
// modelValue.value = '新的值'
</script>

更多用法

// 默认的model (通过 `v-model`)
const modelValue = defineModel() // Ref<any>
modelValue.value = 10

// 增加类型
const modelValue = defineModel<string>() // Ref<string | undefined>
modelValue.value = "hello"

// 带有设置的默认model, 要求非undefined 
const modelValue = defineModel<string>({ required: true }) // Ref<string>

// 特定名称的model (通过 `v-model:count` )
const count = defineModel<number>('count')
count.value++

// 具有默认值的特定名称的model
const count = defineModel<number>('count', { default: 0 }) // Ref<number>

// 本地作用域可变的 model, 顾名思义
// 可以不需要父组件传递v-model
const count = defineModel<number>('count', { local: true, default: 0 })

defineOptions

在之前的版本中如果你需要在<script setup>中定义一些原先 Option Api 的属性比如 inheritAttrs/name 是需要创建一个 script 单独导出这两个属性的。

<script setup lang="ts">
// 一些代码
</script>
<script>
export default {
  name: "hahahha",
  inheritAttrs: false
}
</script> 

现在在 vue3.3 版本中可以用 defineOptions 定义任意选项,但 props, emits, expose, slots 除外。

<script setup lang="ts">
import { getCurrentInstance } from 'vue'
defineOptions({ 
  name: 'hahahha'
  inheritAttrs: false
})
const instance = getCurrentInstance()
console.log(instance?.type.name) // hahahha
// 一些代码
</script>

defineSlots

defineSlotsslots 属性类似,需要提供一个函数语法。

<template>
   <slot msg="hi"></slot>
   <slot name="foo" :age="10"></slot>
</template>

<script setup lang="ts">
  defineSlots<{
    default: (props: { msg: string }) => any
    foo: (props: { age: number }) => any
  }>()
</script>

函数的入参具体如下:

  • key 是 slot 名称
  • value 是 slot 函数
  • 函数的第一个参数是 slot 期望接收的 props,它的类型将用于模板中的 slot props
    defineSlots的返回值与 useSlots 返回的 slots 对象相同。

.tsx 结尾的文件中使用:

import { SlotsType } from 'vue'
import { defineComponent } from 'vue'

export default defineComponent({
  slots: Object as SlotsType<{
    default: { msg: string }
    foo: { age: number }
  }>,
  setup(props, { slots }) {
    return () => (
      <>
        {slots.default && slots.default({ msg: 'hi' }) }
        {slots.foo && slots.foo({ age: 10 })}
      </>
    )
  }
})

上述两种定义的方式在组件中都可以这样使用。

<Sloter>
  <template #default="{ msg }">{{ msg }}</template>
  <template #foo="{ age }">{{ age }}</template>
</Sloter>

defineProps 解构

vue3.3 中此功能是实验性的,当使用该新特性时需要进行以下配置(需要重启):

// vite.config.ts
export default defineConfig({
  plugins: [
    vue({
      script: {
        propsDestructure: true
      }
    })
  ]
})

允许非结构化的 prop 保留响应性:

// Child.vue
<script setup lang="ts">
import type { HI } from './hi'
const { name = 'world' } = defineProps<HI>()
watchEffect(() => {
  console.log('watch', name) // 每次修改值都会触发打印
})
</script>

// Parent.vue
<template>
   <Child :name="name"></Child>
   <button @click="changeName">change name</button>
</template>

<script setup lang="ts">
import Child from './Child.vue'
import { ref } from 'vue'

const name = ref('hi')
function changeName () {
  name.value+='hi'
}
</script>

toRef和toValue增强

toRef

在 vue3.3 版本中 toRef 已得到增强,以支持将 values/getters/refs 规范化为 refs

import { ref, toRef } from 'vue'
const firstRef = toRef(1) // 等价于ref(1)
const second = ref(2)
const secondRef = toRef(second) // existingRef 按原样返回现有的引用
const getterRef = toRef(() => second) // 创建一个readonly ref,在.value访问时调用getter

使用 getter 调用 toRef 类似于 computed,但当 getter 只是执行属性访问而没有昂贵的计算时,效率会更高。

vue3 日常使用中会封装各种组合函数 composition api,通常会遇到响应式丢失的问题,举个例子:

// Child.vue
<script setup lang="ts">
const props = defineProps<{ user: { info: { age: number } } }>()

useXXX(props.user.info.age)	// 非响应式
function useXXX (age) {
  watchEffect(() => {
    console.log('watchEffect', age)	// 只会触发一次
  })
}
</script>

引用 Child 并修改传入的参数:

<template>
   <Child :user="user"></Child>
   <button @click="changeAge">change age</button>
</template>

<script setup lang="ts">
import Child './Child.vue'
import { ref } from 'vue'

const user = ref({
  info: {
    age: 1
  }
})
function changeAge () {
  user.value.info.age += 1
}}
</script>

通过上面的例子可以看到在取值的时候遇到了响应式丢失的问题,那么如何解决上面的问题呢,这时可以使用 toRef,只需要进行如下修改:

useXXX(toRef(props.user.info, 'age'))

这样写的话还是会有一点问题,如下这样修改值是不会触发响应式:

function changeAge () {
  user.value.info = {
    age: 10
  }
}

如何解决上述问题呢:

// 方法一
useXXX(() => props.user.info.age)
function useXXX(age) {
  watchEffect(() => {
    console.log('watchEffect', age())	// 但是要通过方法调用
  })
}

// 方法二
useXXX(computed(() => props.user.info.age))
function useXXX(age) {
  watchEffect(() => {
    console.log('watchEffect', age.value)	// 但是要使用computed
  })
}

// 方法三
useXXX(toRef(() => props.user.info.age))
function useXXX(age) {
  watchEffect(() => {
    console.log('watchEffect', age.value)
  })
}

toValue

新的 toValue 实用程序方法提供了相反的功能,将 values/getters/refs 标准化为值。

const unrefValue = unref(() => 3)	// () => 3
const value1 = toValue(ref(1))	// 1
const value2 = toValue(2)	// 2
const value3 = toValue(() => 3)	 // 3

总结

个人观点:这些更新的新特性有些还是比较实用的,但是其中又包含太多的黑魔法。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值