在之前的文章中,我们在代码里都使用了setup的语法糖,写起来十分简洁方便,但是有些小伙伴对它的用法不是很了解,私信说希望能讲一讲;本文我们就结合typescript,详细讲透setup语法糖的一些用法。
简介 我们知道,setup函数是vue3中的一大特性函数,是组合式API的入口,我们在模板中用到的数据和函数都需要在里面定义,并且最后通过setup函数导出后才能在template中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script> import { ref } from 'vue' import Card from './components/Card'; export default { components: { Card, }, setup(props, ctx){ const count = ref(0); const add = () => { count.value ++ } const sub = () => { count.value ++ } return { count, add, sub, } } } </script>
但是setup函数使用起来比较臃肿,所有的逻辑都写在一个函数中定义;我们发现这样简单的变量和函数,需要频繁的定义导出,再次定义导出,在实际项目开发中会很麻烦,我们写的时候也是需要不断的来回切换,而且变量一多还容易搞混。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
于是更好用的setup语法糖出现了,将setup属性添加到<script>
标签,上面的变量和函数可以通过语法糖简写成如下:
<script setup> import { ref } from 'vue'; const count = ref(0) const add = () => { count.value ++ } const sub = () => { count.value ++ } </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
通过上面的一个简单的小案例,我们就发现setup语法糖不需要显示的定义和导出了,而是直接定义和使用,使代码更加简洁、高效和可维护,使代码更加清晰易读,我们接着来看下还有哪些用法。
基本用法 上面的案例我们已经知道了在setup语法糖中,不需要再繁琐的进行手动导出;不过setup语法糖不支持设置组件名称name,如果需要设置,可以使用两个script标签:
<script> export default { name: 'HomeView', }; </script> <script setup> import { ref } from 'vue'; // ... </script>
如果设置了lang属性,script标签和script setup标签需要设置成相同的属性。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
生命周期 Vue3中取消了create的生命周期函数,在其他的生命周期函数前面加上了on,例如onMounted、onUpdated;同时新增了setup函数替代了create函数,setup函数比mounte函数更早执行,因此我们可以在代码中导入函数钩子,并使用它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script lang="ts" setup> import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured, } from "vue"; onBeforeMount(()=>{ // ... }) </script>
和vue2的8个生命周期函数相比,在setup函数中,排除了beforeCreate和created,加上onActivated和onDeactivated2个在keep-alive中使用的函数钩子,和一个onErrorCaptured异常捕获钩子,一共有9个生命周期的函数钩子可供使用。
响应式 响应式是vue3和vue2比较大的一处不同之处,vue2在data中定义的数据会自动劫持成为响应式,而vue3默认返回的数据不是响应式的,需要通过ref和reactive来定义数据,ref定义简单的数据类型,而reactive定义复杂数据类型,使之成为响应式:
<script lang="ts" setup> import { ref, reactive } from 'vue'; const count = ref(0); const person = reactive({ name: 'jone', age: 18, }) </script>
虽然ref是用来定义简单数据类型,不过对于对象和数组的复杂数据类型也能使用,不过使用时都需要加上.value:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
<script lang="ts" setup> import { ref, reactive } from 'vue'; const list = ref([]); const person = ref({ name: 'jone', age: 18, }); list.value.push(23); console.log(person.value.name) // 报错 // 类型“number”的参数不能赋给类型“object”的参数。 const count = reactive(2) </script>
ref和reactive看起来用法是相同的,但使用ref时,操作变量值的时候需要用.value
,因此适用零散的单个变量;如果是多个相关联的变量,比如用户的一系列信息,姓名、性别、住址等,使用ref定义单个变量较为麻烦,就可以使用reactive组合成对象。
如果我们想要用到复杂数据类型中的某个属性,还想要和原来的数据保持关联,比如person中的name或者age,只通过解构的方式,数据响应性会丢失,页面并不会改变:
<template> <div>{{ name }} {{ age }}</div> </template> <script lang="ts" setup> import { ref, reactive } from 'vue'; const person = reactive({ name: 'jone', age: 18, }); const { name, age } = person; setTimeout(() => { // 页面上的数据并不会响应改变 person.name = "hello"; }, 1500); </script>
这个时候,我们就可以使用toRef()
函数来关联两个变量,这个函数的功能相当于创建了一个ref对象,并将其值指向对象中的某个属性:
<script lang="ts" setup> import { ref, reactive, toRef } from 'vue'; const person = reactive({ name: 'jone', age: 18, }); const name = toRef(person, 'name'); setTimeout(() => { // 页面上的数据随之响应 person.name = "hello"; // 或者直接更改name变量 name = "hello"; }, 1500); </script>
这样,我们更改person中的属性或者直接更改name变量,两者都会随对方的改变而改变;我们发现toRef一次只能创建一个ref对象,如果同时有数个变量,效率不够高,就需要用到toRefs()
:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
<script lang="ts" setup> import { toRefs } from 'vue'; const person = reactive({ name: 'jone', age: 18, }); console.log(toRefs(person)); console.log(person); </script>
toRef只是创建一个ref变量,而toRefs则是创建了一堆ref变量,它的作用是将响应式对象上所有的属性都转换为ref,然后再将这些变量组合成一个对象,因此我们可以打印出来看下,发现toRefs后的数据也只是一个普通的对象,只不过对象中有很多的ref变量:
虽然toRef可以将响应式数据的属性转换成ref对象,不过当toRef和props结合使用的时候,是不允许修改ref对象的值的,因为这样等于直接修改props的数据,这种情况下可以使用下面介绍的带有get/set的computed函数。
<script setup> const title = toRef(props, "title"); const clickChange = () => { // 报警: // Set operation on key "title" failed: target is readonly. title.value = "new title"; }; </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们可以将title改成computed的形式:
<script setup> const title = computed({ get: () => props.title, set: (val) => { emits("update:title", val); }, }); </script>
此外,对于一些复用性高的数据和业务逻辑,我们可以将其封装到组合函数中,所谓的组合式函数,官方的解释如下:
在Vue应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
比如对于分页请求的列表数据tableList和页码等多数页面会用到的复用性高的数据,我们可以选择将其提取到组合式函数中来,这个时候就可以利用toRefs
函数将响应式数据转换成多个ref,同时也不失去响应性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script setup> import { reactive, toRefs } from "vue"; export function usePage() { const state = reactive({ pageNo: 1, pageSize: 10, tableList: [], }); const addPageNo = () => { state.pageNo++; }; const getTableList = () => { // 异步获取列表数据 }; return { ...toRefs(state), addPageNo, getTableList }; } </script>
我们在页面上引入usePage函数,同时解构出其中的数据和函数:
<script setup> import { usePage } from "@/hooks/page"; const { tableList, pageNo, pageSize, addPageNo } = usePage(); </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
computed computed是基于依赖进行缓存的一种属性,用于派生出或者计算出一个值;我们在setup中使用时,需要先引入computed
<script lang="ts" setup> import { ref, computed } from 'vue'; const count = ref(10); const double = computed(() => count.value * 2); </script>
我们给computed函数传入一个箭头函数,箭头函数的返回值作为computed的计算返回;不过此时的double是一个只读属性,在setup中通过.value
获取其值,如果强行改变其值会报错;computed也可以接收一个options,动态设置依赖值:
<script lang="ts" setup> import { ref, computed } from 'vue'; const count = ref(10); const double = computed({ get: () => count.value * 2, set: (val) =>{ count.value = val / 2 } }); // 这样会触发double的set函数 double.value = 16 </script>
watch和watchEffect 在Vue3中,watch和watchEffect都是用来侦听数据源并执行相应操作的函数;其中watch
函数是用来侦听特定的数据源,并在数据源改变时执行回调函数:
<script setup lang="ts"> const name = ref("aa"); watch(name, (newVal, oldVal) => { // ... }); </script>
对于reactive对象中的属性,很多小伙伴理所应当的认为这样写就可以了:
<script setup lang="ts"> const person = reactive({ name: "cc", }); watch(person.name, (newVal, oldVal) => { // ... } ); </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
如果按照上面写法,则会报以下告警信息:
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.
这是因为person.name变量存放的是一个固定的字符串值,watch拿到的参数只是一个字符串,但是字符串并不具备任何响应式的属性;因此上述的报错信息提示了,可以传入一个getter函数、ref值、reactive对象或者以上类型的数组,因此我们可以有以下两种修改方式:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
<script setup> const person = reactive({ name: "cc", }); // 第一种,直接监听对象 watch(person, (newVal, oldVal) => { // ... } ); // 第二种,通过getter函数包裹 watch(() => person.name, (newVal, oldVal) => { // ... } ); </script>
同样的,我们如果要监听多个属性,也可以传入一个数组:
<script setup> watch([() => person.name, count], (val) => { console.log("val", val); }); </script>
那么,有趣的事情来了,如果我们将情况变得更加复杂一些,person中的属性是多层嵌套的复杂对象:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script setup> const person = reactive({ a: { b: { c: "22", }, }, }); watch( person, (val) => { // ... }, ); setTimeout(() => { person.a.b.c = "33"; }, 1.5 * 1000); </script>
如果使用watch监听person中的属性,还是能监听到改变,因为watch会自动对reactive对象开启深度监听;但是用getter函数包裹的嵌套属性,还能吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script setup> const person = reactive({ a: { b: { c: "22", }, }, }); watch( () => person.a.b, (val) => { // ... }, ); setTimeout(() => { person.a.b.c = "33"; }, 1.5 * 1000); </script>
很遗憾,这样并不能监听到,我们需要对多级的属性手动开启深度监听:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
<script setup> watch( () => person.a.b, (val) => { // ... }, { deep: true, } ); </script>
watchEffect函数
则是vue3新增的一个api,用于侦听响应式数据源,发送改变后自动重新运行函数;watchEffect可以观察到函数中所有的响应式数据,并且在这些数据发送改变后自动重新运行函数:
<script setup> const person = reactive({ first: "aa", last: "bb", }); watchEffect(() => { console.log(person.first); console.log(person.last); }); setTimeout(() => { person.first = "bb"; }, 1 * 1000); setTimeout(() => { person.last = "cc"; }, 2 * 1000); </script>
watchEffect监听的任意数据发生变化都会触发函数。
获取组件实例 在有些情况下,我们需要获取元素的dom节点或者子组件的实例对象,比如canvas画图传入dom节点或者调用子组件内部的函数等等,都需要获取节点;在vue2中是通过this.$refs
的方式,vue3中需要通过ref:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div ref="myRef"></div> <my-component ref="myDom"></my-component> </template> <script lang="ts" setup> import { ref, onMounted } from 'vue'; import MyComponent from '@/component/MyComponent'; const myRef = ref(null); const myDom = ref(null); onMounted(() => { // dom元素 console.log(myRef.value); // 组件节点 myDom.value.reload(); }); </script>
我们发现组件import导入后,在模板中就可以直接使用了,不需要再进行注册;给需要操作的节点绑定ref属性,名称和下面ref定义的保持一致;不过需要注意的是,操作dom元素需要在页面mounted之后。
对于for循环中的多个节点,我们可以将ref属性接收一个函数,函数的参数代表了当前循环的元素,将其存储下来,就可以获取多个节点的列表:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
<template> <div v-for="item in list" :key="item" :ref="setRef"></div> </template> <script lang="ts" setup> import { ref , onMounted } from 'vue'; const list = ref([1,2,3,5,7,8]); const refList = ref([]); const setRef = (el) =>{ refList.value.push(el) } </script>
Props 对setup的基础用法有了一定了解,我们来看看setup语法糖的更多用法;首先就是父子组件传数据,子组件需要定义props,通过defineProps
指定props的数据类型,主要有三种写法方式:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <script setup> import { defineProps } from 'vue'; // 第一种 defineProps(['title']); // 第二种 defineProps({ title: String, count: Number, }); // 第三种 defineProps({ title: { type: String, default: '', required: true, } }), </script>
接收到的props可以直接在模板中使用;对于复杂数据类型,比如对象和数组,我们在为其设置默认值的时候,如果只写一个空数组,就会报错:
<script setup> import { defineProps } from 'vue'; // 报错: // Type of the default value for 'list' prop must be a function. defineProps({ list: { type: Array, default: [], }, }), </script>
正确的方式是通过函数的方式返回:
<script setup> import { defineProps } from 'vue'; defineProps({ list: { type: Array, default: () => [], }, data: { type: Object, default: () => ({}), }, }), </script>
对于组合类型的props,可以通过中括号,使用逗号进行分割:
<script setup> defineProps({ title: { type: [String, Number], }, }), </script>
上面的写法,根据官方的说明,称为运行时声明
,也就是在项目运行时才会校验参数的类型是否正确;而使用了typescrip,可以基于类型声明
,这样我们在IDE中传入参数时,立刻就能进行类型推断和检查:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
运行时声明和基于类型声明不可同时使用。
<script setup lang="ts"> interface Props { title: string; count?: number; flag?: boolean; list?: Array<ListItem>; obj: ListItem; } interface ListItem { id: number; name: string; } const props = defineProps<Props>(); </script>
使用defineProps进行基于类型声明的缺点就是不能给props提供默认值,这里还需要用到一个withDefaults
函数进行默认赋值:
<script setup lang="ts"> const props = withDefaults(defineProps<Props>(), { title: "", count: 0, flag: false, list: () => [], obj: () => ({ id: 0, name: "" }), }); </script>
每次用到defineProps
,都需要从vue中引入,这样比较麻烦;很多文章中都会说这是一个宏函数,不需要导入,直接使用;所谓的宏函数也叫编译宏函数,是在作用域内没有定义,而在编译过程中自动注入的工具函数;实际项目中eslint会校验失败,我们需要在eslint配置中开启编译宏:
module .exports = { env : { "vue/setup-compiler-macros" : true , }, };
修改完后需要重启服务器,这样,下面的defineEmits、defineExpose等函数都可以直接使用。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
Emits defineEmits
函数是一个用于定义组件的自定义事件的API,通常用于子组件中;它接受一个参数,可以是一个数组或对象,用于指定需要定义的自定义事件。
如果传入的是一个数组,数组的每个元素就是一个字符串,表示一个自定义事件的名称:
<script setup> const emits = defineEmits(["add", "sub"]); const count = ref(0); const clickAddBtn = () => { emits("add", count.value++); }; const clickSubBtn = () => { emits("sub", count.value++); }; </script>
在父组件中我们就可以定义使用@add
和@sub
的回调函数了。而如果我们传入一个对象,对象的键就是自定义事件的名称,值可以是一个函数,用于验证自定义事件的参数类型。
<script setup> const emits = defineEmits({ customEvent: (res) => { console.log("事件数据:", res); return res > 5; }, }); const clickBtn = () => { emits("customEvent", Math.floor(Math.random() * 10)); }; </script>
在上面的代码中,我们定义了自定义事件customEvent
;当该事件被触发时,就会调用customEvent后面定义的函数,打印出负载数据,同时,我们可以在customEvent函数中返回一个Boolean类型,对响应数据进行校验,如果返回false,数据校验不通过,会在控制台进行提示:
[Vue warn]: Invalid event arguments: event validation failed for event “customEvent”.
defineEmits写法也分为运行时声明和基于类型声明,使用基于类型声明同样需要在函数后面跟上数据类型,使用e声明函数的名称:
<script lang="ts" setup> const emits = defineEmits<{ (e: "click", data: number, data1: number): void; (e: "custom"): void; }>(); const clickBtn = () => { emits("click", 2, 3); }; </script>
不过,这样的写法不是很友好,而vue3.3引入了一种更符合人体工程学
的声明方式,写法更加友好:
<script lang="ts" setup> const emit = defineEmits<{ foo: [id: number] bar: [name: string, ...rest: any[]] }>() </script>
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
Expose 在vue2中,如果父组件需要调用子组件的方法,直接使用this.$refs.child.getData(),就可以调用;但是在vue3中,子组件默认都不会暴露任何数据和方法,需用通过defineExpose
函数定义后才能拿到:
// Child.vue <script setup> const count = ref(2); const fn = () => { console.log("1"); }; defineExpose({ fn, count, }); </script>
父组件通过上面ref的方式获取组件实例,即可调用子组件暴露的方法;
// Parent.vue <script lang="ts" setup> const childRef = ref(null); onMounted(() => { childRef.value.fn(); console.log(childRef.value.count); }); </script>
同样的,defineExpose也支持基于类型声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <script lang="ts" setup> import { Ref } from 'vue'; const count = ref(2); const fn = () => { return 2; }; interface FORM { id: number; name: string; note: string; } const form = reactive<FORM>({ id: 0, name: "", note: "", }); defineExpose<{ count: Ref; form: FORM; fn: () => number; }>({ form, count, fn, }); </script>
自定义指令 我们回顾一下,在vue2中,挂载全局指令通过directive
函数,直接挂载到Vue对象:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
<script> Vue.directive('auth', { // ... }) </script>
而在vue3中,通过createApp
创建实例,因此通过app.directive
函数进行挂载全局指令:
<script> app.directive("focus", { mounted(el: HTMLElement) { el?.focus(); }, }); </script>
而在setup语法糖中引入自定义指令,我们需要将引入的指令名称定义成v
为前缀的小驼峰形式,引入后不用注册,直接在模板中通过小写的中划线连接使用即可:
<template> <input type="text" v-input-focus /> </template> <script lang="ts" setup> import vInputFocus from "@/directive/focus"; </script>
slots和attrs Vue中插槽slot是一种特殊的内置标签,它允许父组件向子组件内部插入自定义的html内容,使得父组件可以在不修改子组件的情况下,非常灵活向子组件中动态的添加修改内容;在vue2使用this.$slots
对象来获取插槽,而在setup语法糖中,我们就要用到useSlots
函数。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
useSlots
函数可能很多小伙伴比较陌生,大部分场景下我们直接使用<slot />
标签即可;而在一些特殊的渲染场景下,就需要useSlots在JSX中渲染插槽数据;比如一些组件的属性支持JSX代码,我们可以用来渲染一些插槽:
// Child.vue <script setup> import { ref, useSlots, } from "vue"; const slots = useSlots(); import { NDataTable } from "naive-ui"; const columns = ref([ { title: "Action", key: "action", render(row) { return h("div", null, slots.title ? slots.title() : slots.default()); }, }, ]); </script>
我们通过useSlots获取slots
对象,默认会有一个default属性,就是我们的默认插槽;如果我们向子组件中插入其他命名插槽,slots对象会有相应的属性,比如这里我们在父组件使用title插槽,
<template> <Child> <p> i am default slot</p> <template #title> <p>i am title slot</p> </template> </Child> </template>
打印slots对象查看,我们发现有两个属性:
Proxy (Object ) { default : (...args ) => {…}, title : (...args ) => {…} }
回到上面的案例代码,我们可以判断slots.title
属性是否存在,也就是插槽是否存在,然后通过h函数渲染slots.title()
。
另外一个有些类似的属性就是attrs,可以用来捕获任何我们没有在组件中声明的参数,我们在setup语法糖中也是使用useAttrs
来获取它:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
// Parent.vue <template> <Child title="ceshi" msg="msg11"></Child> </template> <script setup> import Child from './Child.vue' </script>
// Child.vue <script setup> import { useAttrs } from "vue"; const attrs = useAttrs(); console.log("attrs", attrs.title, attrs.msg); </script>
如果我们在Child.vue将title定义到props中后,attrs就不会出现title属性。
总结 本文整理总结了setup语法糖的一些用法,主要包括响应式、props、emit、expose和slot,由于篇幅的限制,响应式中还有很多函数,包括isRef、unref、toRaw等这里不再详细介绍;setup语法糖的优势在于能够使得代码更简洁,可读性强,同时可以将复杂的逻辑和状态管理通过组合式函数拆分为小的、可复用模块,使得代码更加模块化。因此在vue3中,掌握并合理的利用setup语法糖可以帮助我们更好的组织和管理代码,提高开发效率。