前言

vue3.3发布了,针对之前script setup语法糖做了许多优化,解决了一些痛点,让我感觉这写法终于能正儿八经用一用了,下面就是一些必学的知识点介绍了。

官方博客说明文档:Announcing Vue 3.3

Volar插件

为了完整体验vue3.3的特性,我们需要将Volar插件升级到测试版v1.7.8及以上。

利用组件泛型实现自动推断类型?

现在组件的script上可以配置一个generic属性,用来接受泛型参数,但是我感觉官方给的示例代码没有什么实战意义。

generic可以接受多个泛型,用逗号分隔,你甚至可以利用extends做一个接受的类型约束。

<script setup lang="ts" generic="T extends string | number, U extends Item">
import type { Item } from "./types";

defineProps<{
  id: T;
  list: U[];
}>();

</script>

这是一个官方的示例,我们甚至可以import引入外部类型来使用,显然这里不符合先引再用的调用规则,但是官方会在真实使用的时候做特殊处理,所以不用太担心。

看上去很美好,但是它好像并没有解决什么问题,比如props还是得有明确的类型声明,T明显要求是个基本类型,U还得约束于Item,显然它不能帮助我们减少props类型声明。

但是它也许可以帮助我们去实现slot插槽参数类型声明,在一些复杂的场景中,我们可能会抽象出一个插槽的位置,这个位置用于用户自定义,那么就需要将参数返回给父组件中去,这个数据的类型也许可以用到泛型。

但是目前来说,这个特性可能还需要观望一阵子。

defineProps解构和默认值

这个特性需要手动开启,vite.config.ts中:

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

export default defineConfig({
  plugins: [
    vue({
      script: {
        propsDestructure: true  //手动开启
      }
    })
  ],
});

配置完毕后记得重启下项目,如果配置了eslint,我们还需要手动配置一个规则:

.eslintrc.cjs

/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");

module.exports = {
  root: true,
  extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/eslint-config-typescript",
    "@vue/eslint-config-prettier/skip-formatting"
  ],
  parserOptions: {
    ecmaVersion: "latest"
  },
  rules: {
    "vue/no-setup-props-destructure": "off",  //配置这个
  }
};

这样就不会报错了,因为vue3一开始就不允许props可以解构,那样会丢失响应式,现在3.3版本做了特殊处理,解构的值会被转为响应式对象。

<template>
  <div>{{ names }}</div>
</template>

<script lang="ts" setup generic="T">
const { names = "默认值" } = defineProps<{
  names: T;
}>();
</script>

当我父组件没有传参时会使用默认值。

但是控制台会报警告

因为我们声明类型的时候,是个必填项,我们调整一下

<template>
  <div>{{ names }}</div>
</template>

<script lang="ts" setup generic="T">
const { names = "默认值" } = defineProps<{
  names?: T;
}>();
</script>

问题解决。

解构出来的值我们打印,发现是string类型,但是如果我们将其放入watch,computed中,却可以监听到变化。

父组件:

<template>
  <div>
    <TestC :names="names" />
    <button @click="onClick">change</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import TestC from "@/components/TestC.vue";

const names = ref("测试");
function onClick() {
  names.value = "测试" + Math.random();
}
</script>

TestC 组件:

<template>
  <div>
    <div>{{ names }}</div>
    <div>{{ comNames }}</div>
  </div>
</template>

<script lang="ts" setup generic="T">
import { computed, watch } from "vue";

const { names = "默认值" } = defineProps<{
  names?: T;
}>();

console.log(names);

const comNames = computed(() => `computed: ${names}`);
watch(
  () => names,
  (newVal, oldVal) => {
    console.log("🚀 ~ file: TestC.vue:28 ~ newVal,oldVal:", newVal, oldVal);
  }
);
</script>

效果还是很惊艳的,明显是在编译时做了处理。

更加便捷的defineEmits类型声明

之前的:

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

其中e是事件的名字,剩下的则是事件的参数。

现在调整为:

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

现在key为事件名,值为事件参数 类型。

目前支持这两种声明模式,但是不能混用,如果你的emits返回值都是void,那么推荐使用新版本的方式,会更加符合代码认知。

新增 defineOptions

之前一直不怎么喜欢用setup语法糖,是因为一些缺失了options配置项,比如配置组件的name属性,使用setup语法糖是没法做到的,因为整个script都是setup函数,没有同级选项了。

于是不得不出现这么一种情况,一个vue组件中,会有两个script声明,一个用于配置options,比如组件名称之类的,一个用来setup语法糖。

巨难受好吧!

现在新增量这么一个配置,专门来解决setup语法糖没有同级属性的问题,方便的很。

defineOptions({
  name: "MyTestC",
  inheritAttrs: false
});

静态常量提升

defineOptions的官方文档中有这么一句话:无法访问 <script setup> 中不是字面常数的局部变量,顾名思义,说明defineOptions中是可以使用变量的,但是这个变量必须是个常量。

本来defineOptions会被提取提升,那么它如何获取到setup中的变量呢,其实就是因为实现了静态常量提升。

用这个功能作者 三咲智子 的例子来说明一下:

<template>
  <div id="title">Hello World</div>
</template>

这段代码最终会被编译为:

const _hoisted_1 = { id: 'title' }
function render(_ctx, _cache) {
  return _openBlock(), _createElementBlock('div', _hoisted_1, 'Hello World')
}

其中_hoisted_1 变量就是被编译器故意提升到顶层的代码。如果关闭此静态常量提升,则它会在 render 函数内。

而setup中的常量也是同理,也会被提升到顶层,所以defineOptions才能拿到。

新增 defineModel

这个就确实有点香了啊,可惜只有setup语法糖才能用,过分啊。

这个也是实验性功能,我们需要手动开启:

vite.config.ts中:

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

export default defineConfig({
  plugins: [
    vue({
      script: {
        defineModel: true, //手动开启
      }
    })
  ],
});

我们来看一下我们以前声明一个v-model需要多少代码:

<template>
  <div>{{ show }}</div>
</template>

<script lang="ts" setup>
import { computed } from "vue";

const props = defineProps<{
  modelValue: boolean;
}>();

const emit = defineEmits<{
  "update:modelValue": [value: boolean];
}>();

const show = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit("update:modelValue", value);
  }
});
</script>

你需要声明props和emit,为了方便使用我们还需要一个computed做数据拦截。

现在的变化:

<template>
  <div>{{ show }}</div>
</template>

<script lang="ts" setup>
const show = defineModel();
</script>

defineModel默认就是用modelValue字段,如果你想指定名字:

<template>
  <div>{{ show }} {{ myShow }}</div>
</template>

<script lang="ts" setup>
const show = defineModel();
const myShow = defineModel("myShow");
</script>

父组件使用时:

<TestC v-model="show" v-model:myShow="show" />

巨方便了。

新增 toRef 和 toValue

vue3之前提供了一个toRefs的方法,用于将reactive对象转为可以解构的普通对象,并且保证数据的响应式。

但是toRefs转的是整个对象,而新增toRef方法可以处理单个指定属性。

<template>
  <div>
    <div>{{ age }}</div>
    <button @click="age++">+1</button>
    <div>{{ a }}</div>
    <button @click="a++">+1</button>
  </div>
</template>

<script lang="ts" setup>
import { reactive, toRef } from "vue";

const test = reactive({
  name: "test",
  age: 18,
  data: {
    a: 1
  }
});

const age = toRef(test, "age");
const a = toRef(test.data, "a");
</script>

如果属性层级过深,参考a的写法,toRef的第二个值只能是单个属性名。

除了这种用法,toRef还支持只读写法,也就是getter函数。

这种方式解决了一个问题,当我们书写hooks或者其他方法的时候,可能需要接一个参数,这个参数可能是props的属性,但是如果我们直接通过props.xxx的方式传入,就会丢失响应式数据,得到一个源数据。

于是可能会有人创建一个ref对象,或者使用computed做一个计算属性,但是它们都不完美,ref是可以修改的,计算属性会有代价,因为它有缓存处理。

于是这次新增的toRef就是为了解决这个问题:

<script lang="ts" setup>
import { toRef } from "vue";

const props = defineProps({
  myShow: Boolean
});

const myShow = toRef(() => props.myShow);

console.log(myShow);  //GetterRefImpl xxx
</script>

打印出来的是一个GetterRefImpl对象,这个对象只读不能修改,符合props不允许直接修改的要求,而数据又能响应式。

而新增的toValue从名字的角度来说,它就是取值的意思,所以他可以这样:

toValue(1) //       --> 1
toValue(ref(1)) //  --> 1
toValue(() => 1) // --> 1

而以前的unref更多的是用于获取原值:

unref(1) //       --> 1
unref(ref(1)) //  --> 1
unref(() => 1) // --> () => 1

所以toValue更适合处理GetterRefImpl,它们的关系:

  • toRef ----- toValue
  • ref ------ unref

其他知识

defineSlots 支持类型参数

文档:typed-slots-with-defineslots

jsx 可以指定全局类型

用于解决react和vue对于jsx的类型定义冲突,由于两个都是定义的全局,混用的情况会出现类型定义的冲突,vue3.4之后删除了jsx的全局类型定义,可以手动指定。

文档:JSX Import Source Support

分类: vue3 快速上手 标签: jsxvue3.3genericdefinePropsdefineEmitsdefineOptions静态常量提升defineModeltoReftoValuedefineSlots

评论

暂无评论数据

暂无评论数据

目录