学好setup语法糖,快速上手Vue3

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

简介

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

vue 复制代码
<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>标签,上面的变量和函数可以通过语法糖简写成如下:

vue 复制代码
<script setup>
import { ref } from 'vue';
const count = ref(0)
const add = () => {
    count.value ++
}
const sub = () => {
    count.value ++
}
</script>

通过上面的一个简单的小案例,我们就发现setup语法糖不需要显示的定义和导出了,而是直接定义和使用,使代码更加简洁、高效和可维护,使代码更加清晰易读,我们接着来看下还有哪些用法。

基本用法

上面的案例我们已经知道了在setup语法糖中,不需要再繁琐的进行手动导出;不过setup语法糖不支持设置组件名称name,如果需要设置,可以使用两个script标签:

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

vue 复制代码
<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定义复杂数据类型,使之成为响应式:

vue 复制代码
<script lang="ts" setup>
import { ref, reactive } from 'vue';
const count = ref(0);
const person = reactive({
    name: 'jone',
    age: 18,
})
</script>

虽然ref是用来定义简单数据类型,不过对于对象和数组的复杂数据类型也能使用,不过使用时都需要加上.value:

vue 复制代码
<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,只通过解构的方式,数据响应性会丢失,页面并不会改变:

vue 复制代码
<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对象,并将其值指向对象中的某个属性:

vue 复制代码
<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()

vue 复制代码
<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函数。

vue 复制代码
<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的形式:

vue 复制代码
<script setup>
const title = computed({
  get: () => props.title,
  set: (val) => {
    emits("update:title", val);
  },
});
</script>

此外,对于一些复用性高的数据和业务逻辑,我们可以将其封装到组合函数中,所谓的组合式函数,官方的解释如下:

在Vue应用的概念中,"组合式函数"(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

比如对于分页请求的列表数据tableList和页码等多数页面会用到的复用性高的数据,我们可以选择将其提取到组合式函数中来,这个时候就可以利用toRefs函数将响应式数据转换成多个ref,同时也不失去响应性:

vue 复制代码
<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函数,同时解构出其中的数据和函数:

vue 复制代码
<script setup>
import { usePage } from "@/hooks/page";

const { tableList, pageNo, pageSize, addPageNo } = usePage();
</script>

computed

computed是基于依赖进行缓存的一种属性,用于派生出或者计算出一个值;我们在setup中使用时,需要先引入computed

vue 复制代码
<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,动态设置依赖值:

vue 复制代码
<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函数是用来侦听特定的数据源,并在数据源改变时执行回调函数:

vue 复制代码
<script setup lang="ts">
const name = ref("aa");

watch(name, (newVal, oldVal) => {
  // ...
});
</script>

对于reactive对象中的属性,很多小伙伴理所应当的认为这样写就可以了:

vue 复制代码
<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对象或者以上类型的数组,因此我们可以有以下两种修改方式:

vue 复制代码
<script setup>
const person = reactive({
  name: "cc",
});
// 第一种,直接监听对象
watch(person, (newVal, oldVal) => {
    // ...
  }
);
// 第二种,通过getter函数包裹
watch(() => person.name, (newVal, oldVal) => {
    // ...
  }
);
</script>

同样的,我们如果要监听多个属性,也可以传入一个数组:

vue 复制代码
<script setup>
watch([() => person.name, count], (val) => {
  console.log("val", val);
});
</script>

那么,有趣的事情来了,如果我们将情况变得更加复杂一些,person中的属性是多层嵌套的复杂对象:

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

vue 复制代码
<script setup>
const person = reactive({
  a: {
    b: {
      c: "22",
    },
  },
});

watch(
  () => person.a.b,
  (val) => {
    // ...
  },
);

setTimeout(() => {
  person.a.b.c = "33";
}, 1.5 * 1000);
</script>

很遗憾,这样并不能监听到,我们需要对多级的属性手动开启深度监听:

vue 复制代码
<script setup>
watch(
  () => person.a.b,
  (val) => {
    // ...
  },
  {
    deep: true,
  }
);
</script>

watchEffect函数则是vue3新增的一个api,用于侦听响应式数据源,发送改变后自动重新运行函数;watchEffect可以观察到函数中所有的响应式数据,并且在这些数据发送改变后自动重新运行函数:

vue 复制代码
<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:

vue 复制代码
<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属性接收一个函数,函数的参数代表了当前循环的元素,将其存储下来,就可以获取多个节点的列表:

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

vue 复制代码
<script setup>
import { defineProps } from 'vue';

// 第一种
defineProps(['title']);
// 第二种
defineProps({
  title: String,
  count: Number,
});
// 第三种
defineProps({
  title: {
    type: String,
    default: '',
    required: true,
  }
}),
</script>

接收到的props可以直接在模板中使用;对于复杂数据类型,比如对象和数组,我们在为其设置默认值的时候,如果只写一个空数组,就会报错:

vue 复制代码
<script setup>
import { defineProps } from 'vue';
// 报错:
// Type of the default value for 'list' prop must be a function.
defineProps({
  list: {
    type: Array,
    default: [],
  },
}),
</script>

正确的方式是通过函数的方式返回:

vue 复制代码
<script setup>
import { defineProps } from 'vue';
defineProps({
  list: {
    type: Array,
    default: () => [],
  },
  data: {
    type: Object,
    default: () => ({}),
  },
}),
</script>

对于组合类型的props,可以通过中括号,使用逗号进行分割:

vue 复制代码
<script setup>
defineProps({
  title: {
    type: [String, Number],
  },
}),
</script>

上面的写法,根据官方的说明,称为运行时声明,也就是在项目运行时才会校验参数的类型是否正确;而使用了typescrip,可以基于类型声明,这样我们在IDE中传入参数时,立刻就能进行类型推断和检查:

运行时声明和基于类型声明不可同时使用。

vue 复制代码
<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函数进行默认赋值:

vue 复制代码
<script setup lang="ts">
const props = withDefaults(defineProps<Props>(), {
  title: "",
  count: 0,
  flag: false,
  list: () => [],
  obj: () => ({ id: 0, name: "" }),
});
</script>

每次用到defineProps,都需要从vue中引入,这样比较麻烦;很多文章中都会说这是一个宏函数,不需要导入,直接使用;所谓的宏函数也叫编译宏函数,是在作用域内没有定义,而在编译过程中自动注入的工具函数;实际项目中eslint会校验失败,我们需要在eslint配置中开启编译宏:

javascript 复制代码
// .eslintrc.js
module.exports = {
  env: {
    // 新增以下
    "vue/setup-compiler-macros": true,
  },
};

修改完后需要重启服务器,这样,下面的defineEmits、defineExpose等函数都可以直接使用。

Emits

defineEmits函数是一个用于定义组件的自定义事件的API,通常用于子组件中;它接受一个参数,可以是一个数组或对象,用于指定需要定义的自定义事件。

如果传入的是一个数组,数组的每个元素就是一个字符串,表示一个自定义事件的名称:

vue 复制代码
<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的回调函数了。而如果我们传入一个对象,对象的键就是自定义事件的名称,值可以是一个函数,用于验证自定义事件的参数类型。

vue 复制代码
<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声明函数的名称:

vue 复制代码
<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引入了一种更符合人体工程学的声明方式,写法更加友好:

vue 复制代码
<script lang="ts" setup>
const emit = defineEmits<{
  foo: [id: number]
  bar: [name: string, ...rest: any[]]
}>()
</script>

Expose

在vue2中,如果父组件需要调用子组件的方法,直接使用this.$refs.child.getData(),就可以调用;但是在vue3中,子组件默认都不会暴露任何数据和方法,需用通过defineExpose函数定义后才能拿到:

vue 复制代码
// Child.vue
<script setup>
const count = ref(2);
const fn = () => {
  console.log("1");
};

defineExpose({
  fn,
  count,
});
</script>

父组件通过上面ref的方式获取组件实例,即可调用子组件暴露的方法;

vue 复制代码
// Parent.vue
<script lang="ts" setup>
const childRef = ref(null);
onMounted(() => {
  childRef.value.fn();
  console.log(childRef.value.count);
});
</script>

同样的,defineExpose也支持基于类型声明:

vue 复制代码
<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对象:

vue 复制代码
<script>
Vue.directive('auth', {
  // ...
})
</script>

而在vue3中,通过createApp创建实例,因此通过app.directive函数进行挂载全局指令:

vue 复制代码
<script>
app.directive("focus", {
  mounted(el: HTMLElement) {
    el?.focus();
  },
});
</script>

而在setup语法糖中引入自定义指令,我们需要将引入的指令名称定义成v为前缀的小驼峰形式,引入后不用注册,直接在模板中通过小写的中划线连接使用即可:

vue 复制代码
<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代码,我们可以用来渲染一些插槽:

vue 复制代码
// 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插槽,

vue 复制代码
<template>
  <Child>
    <p> i am default slot</p>
    <template #title>
      <p>i am title slot</p>
    </template>
  </Child>
</template>

打印slots对象查看,我们发现有两个属性:

javascript 复制代码
Proxy(Object) {
  default: (...args) => {...},
  title: (...args) => {...}
}

回到上面的案例代码,我们可以判断slots.title属性是否存在,也就是插槽是否存在,然后通过h函数渲染slots.title()

另外一个有些类似的属性就是attrs,可以用来捕获任何我们没有在组件中声明的参数,我们在setup语法糖中也是使用useAttrs来获取它:

vue 复制代码
// Parent.vue
<template>
 <Child title="ceshi" msg="msg11"></Child>
</template>
<script setup>
import Child from './Child.vue'
</script>
vue 复制代码
// 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语法糖可以帮助我们更好的组织和管理代码,提高开发效率。

如果觉得写得还不错,敬请关注我的掘金主页。更多文章请访问谢小飞的博客

相关推荐
鑫~阳33 分钟前
html + css 淘宝网实战
前端·css·html
Catherinemin37 分钟前
CSS|14 z-index
前端·css
漫天转悠38 分钟前
Vue3项目中引入TailwindCSS(图文详情)
vue.js
qq_589568102 小时前
Echarts+vue电商平台数据可视化——后台实现笔记
vue.js·信息可视化·echarts
2401_882727572 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder2 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂2 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand3 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL3 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿3 小时前
react防止页面崩溃
前端·react.js·前端框架