10

学好setup语法糖,快

 9 months ago
source link: https://xieyufei.com/2023/10/19/Vue3-Setup.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

学好setup语法糖,快速上手Vue3 - 谢小飞的博客

学好setup语法糖,_

2023年10月19日 中午

5.7k 字

77 分钟

  在之前的文章中,我们在代码里都使用了setup的语法糖,写起来十分简洁方便,但是有些小伙伴对它的用法不是很了解,私信说希望能讲一讲;本文我们就结合typescript,详细讲透setup语法糖的一些用法。

  我们知道,setup函数是vue3中的一大特性函数,是组合式API的入口,我们在模板中用到的数据和函数都需要在里面定义,并且最后通过setup函数导出后才能在template中使用:

<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函数更早执行,因此我们可以在代码中导入函数钩子,并使用它们:

<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变量:

toRefs

toRefs

  虽然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,同时也不失去响应性:

<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中的属性是多层嵌套的复杂对象:

<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函数包裹的嵌套属性,还能吗?

<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:

<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的数据类型,主要有三种写法方式:

<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配置中开启编译宏:

// .eslintrc.js
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也支持基于类型声明:

<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语法糖可以帮助我们更好的组织和管理代码,提高开发效率。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK