使用Vue2.x的小伙伴都熟悉,Vue2.x中所有数据都是定义在data中,方法定义在methods中的,并且使用this来调用对应的数据和方法。那Vue3.x中就可以不这么玩了, 具体怎么玩我们后续再说, 先说一下Vue2.x版本这么写有什么缺陷,所以才会进行升级变更的。

文档

https://vue3js.cn/vue-composition-api/

回顾 Vue2.x 实现加减

  1. <template>
  2. <div class="homePage">
  3. <p>count: {{ count }}</p>
  4. <p>倍数: {{ multiple }}</p>
  5. <div>
  6. <button style="margin-right:10px" @click="increase">加1</button>
  7. <button @click="decrease">减一</button>
  8. </div>
  9. </div>
  10. </template>
  11. <script>
  12. export default {
  13. data() {
  14. return {
  15. count: 0,
  16. };
  17. },
  18. computed: {
  19. multiple() {
  20. return 2 * this.count;
  21. },
  22. },
  23. methods: {
  24. increase() {
  25. this.count++;
  26. },
  27. decrease() {
  28. this.count++;
  29. },
  30. },
  31. };
  32. </script>

上面代码只是实现了对count的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:

Vue3 Composition 组合式 API - 图1

当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:

Vue3 Composition 组合式 API - 图2

甚至一个功能还有会依赖其他功能,全搅合在一起。
当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转,这其中的的痛苦写过的都知道。
那我们就想啊, 如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:

Vue3 Composition 组合式 API - 图3

那么vue2.x版本给出的解决方案就是Mixin, 但是使用Mixin也会遇到让人苦恼的问题:

  1. 命名冲突问题
  2. 不清楚暴露出来的变量的作用
  3. 逻辑重用到其他 component 经常遇到问题

关于上面经常出现的问题我就不一一举例了,使用过的小伙伴多多少少都会遇到。文章的重点不是Mixin,如果确实想知道的就留言啦~

所以,我们Vue3.x就推出了Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点认识Composition API

Composition API

image.png

setup()

setup函数是一个新的组件选项。作为在组件内使用**Composition API**的入口点。从生命周期钩子的视角来看,它会在beforeCreate钩子之前被调用,所有变量、方法都在setup函数中定义,之后return出去供外部使用。从setup返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

  1. export default defineComponent ({
  2. beforeCreate() {
  3. console.log("----beforeCreate----");
  4. },
  5. created() {
  6. console.log("----created----");
  7. },
  8. setup() {
  9. console.log("----setup----");
  10. },
  11. })

image.png
由于在执行setup 时尚未创建组件实例,因此在 setup 选项中没有 this

你只能访问以下 property:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

该函数有2个参数:

  • props
  • context

    Props 传值

  1. <!-- 组件传值 -->
  2. <com-setup p1="传值给 com-setup"/>
  1. // 通过 setup 函数的第一个形参,接收 props 数据:
  2. setup(props) {
  3. console.log(props)
  4. },
  5. // 在 props 中定义当前组件允许外界传递过来的参数名称:
  6. props: {
  7. p1: String
  8. }

setup中接受的props是响应式的, 当传入新的props 时,会及时被更新。由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。

错误代码示例, 这段代码会让props不再支持响应式:

  1. // demo.vue
  2. export default defineComponent ({
  3. setup(props, context) {
  4. const { name } = props
  5. console.log(name)
  6. },
  7. })

通过toRefs、toRef解构props

如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作

  1. import { toRefs } from 'vue'
  2. setup(props) {
  3. const { title } = toRefs(props)
  4. console.log(title.value)
  5. }

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

  1. // MyBook.vue
  2. import { toRef } from 'vue'
  3. setup(props) {
  4. const title = toRef(props, 'title')
  5. console.log(title.value)
  6. }

Context

setup 函数的第二个形参 context 是一个上下文对象,前面说了setup中不能访问Vue2中最常用的this对象,所以context中提供属性(attrsslotsemitparentroot),其对应于vue2中的this.$attrsthis.$slotsthis.$emitthis.$parentthis.$root

在 vue 3.x 中,它们的访问方式如下:

  1. setup(props, context) {
  2. console.log(context)
  3. // Attribute (非响应式对象)
  4. console.log(context.attrs)
  5. // 插槽 (非响应式对象)
  6. console.log(context.slots)
  7. // 触发事件 (方法)
  8. console.log(context.emit)
  9. console.log(this) // undefined
  10. },
  11. /*
  12. attrs: Object
  13. emit: ƒ ()
  14. listeners: Object
  15. parent: VueComponent
  16. refs: Object
  17. root: Vue
  18. ...
  19. */

解构context

  1. export default {
  2. setup (props, { emit }) {
  3. const handleUpdate = () => {
  4. emit('update', 'Hello World')
  5. }
  6. return { handleUpdate }
  7. }
  8. }

setup也用作在tsx中返回渲染函数:

  1. setup(props, { attrs, slots }) {
  2. return () => {
  3. const propsData = { ...attrs, ...props } as any;
  4. return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
  5. };
  6. },

注意:this关键字在setup()函数内部不可用,在方法中访问setup中的变量时,直接访问变量名就可以使用。

为什么props没有被包含在上下文中?

  1. 组件使用props的场景更多,有时甚至只需要使用props
  2. 将props独立出来作为一个参数,可以让TypeScript对props单独做类型推导,不会和上下文中其他属性混淆。这也使得setup、render和其他使用了TSX的函数式组件的签名保持一致。

使用渲染函数

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:

  1. import { h, ref, reactive } from 'vue'
  2. export default {
  3. setup() {
  4. const readersNumber = ref(0)
  5. const book = reactive({ title: 'Vue 3 Guide' })
  6. // Please note that we need to explicitly expose ref value here
  7. return () => h('div', [readersNumber.value, book.title])
  8. }
  9. }

reactive(), ref() 创建响应式数据

在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,作用等同于在vue2中的data,不同的是他们使用了ES6Porxy API解决了vue2 defineProperty 无法监听数组和对象新增属性的痛点,而且使任何响应式变量在任何地方起作用。

  1. <template>
  2. <div class="contain">
  3. <el-button type="primary" @click="numadd">add</el-button>
  4. <span>{{ `${state.str}-${num}` }}</span>
  5. </div>
  6. </template>
  7. <script lang="ts">
  8. import { reactive, ref } from 'vue';
  9. interface State {
  10. str: string;
  11. list: string[];
  12. }
  13. export default {
  14. setup() {
  15. const state = reactive<State>({
  16. str: 'test',
  17. list: [],
  18. });
  19. //ref需要加上value才能获取
  20. const num = ref(1);
  21. console.log(num) // { value: 1 }
  22. console.log(num.value) // 1
  23. const numadd = () => {
  24. num.value++;
  25. };
  26. return { state, numadd, num };
  27. },
  28. method:{
  29. numAdd(){
  30. this.num++ //setup return的就是value,所以这里不需要加value
  31. }
  32. }
  33. };
  34. </script>

reactive函数可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean等。

image.png

上面的代码中,我们绑定到页面是通过user.name,user.age;这样写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢?答案是不能直接对**user**进行解构, 这样会消除它的响应式, 这里就和上面我们**props**不能使用ES6直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用**toRefs**

toRefs()

将传入的reactive对象里所有的属性都转化为响应式数据对象(ref)

使用reactive return 出去的值每个都需要通过reactive对象 .属性的方式访问非常麻烦,我们可以通过解构赋值的方式范围,但是直接解构的参数不具备响应式,此时可以使用到这个api(也可以对props中的响应式数据做此处理)

将前面的例子作如下👇修改使用起来更加方便:

  1. <template>
  2. <div class="contain">
  3. <el-button type="primary" @click="numadd">add</el-button>
  4. - <span>{{ `${state.str}-${num}` }}</span>
  5. + <span>{{ `${str}-${num}` }}</span>
  6. </div>
  7. </template>
  8. <script lang="ts">
  9. import { reactive, ref, toRefs } from 'vue';
  10. interface State {
  11. str: string;
  12. list: string[];
  13. }
  14. export default {
  15. setup() {
  16. const state = reactive<State>({
  17. str: 'test',
  18. list: [],
  19. });
  20. //ref需要加上value才能获取
  21. const num = ref(1);
  22. const numadd = () => {
  23. num.value++;
  24. };
  25. - return { state, numadd, num };
  26. + return { ...toRefs(state), numadd, num };
  27. },
  28. };
  29. </script>

具体使用方式如下:

image.png

toRef()

toRef 用来将引用数据类型或者reavtive数据类型中的某个属性转化为响应式数据

reactive 数据类型

  1. /* reactive数据类型 */
  2. let obj = reactive({ name: '小黄', sex: '1' });
  3. let state = toRef(obj, 'name');
  4. state.value = '小红';
  5. console.log(obj.name); // 小红
  6. console.log(state.value); // 小红
  7. obj.name = '小黑';
  8. console.log(obj.name); // 小黑
  9. console.log(state.value); // 小黑

引用数据类型

  1. <template>
  2. <span>ref----------{{ state1 }}</span>
  3. <el-button type="primary" @click="handleClick1">change</el-button>
  4. <!-- 点击后变成小红 -->
  5. <span>toRef----------{{ state2 }}</span>
  6. <el-button type="primary" @click="handleClick2">change</el-button>
  7. <!-- 点击后还是小黄 -->
  8. </template>
  9. <script>
  10. import { ref, toRef, reactive } from 'vue';
  11. export default {
  12. setup() {
  13. let obj = { name: '小黄' };
  14. const state1 = ref(obj.name); // 通过ref转换
  15. const state2 = toRef(obj, 'name'); // 通过toRef转换
  16. const handleClick1 = () => {
  17. state1.value = '小红';
  18. console.log('obj:', obj); // obj:小黄
  19. console.log('ref', state1); // ref:小红
  20. };
  21. const handleClick2 = () => {
  22. state2.value = '小红';
  23. console.log('obj:', obj); // obj:小红
  24. console.log('toRef', state2); // toRef:小红
  25. };
  26. return { state1, state2, handleClick1, handleClick2 };
  27. },
  28. };
  29. </script>

Vue3 Composition 组合式 API - 图8

https://mp.weixin.qq.com/s/avfb-jJeW7f_tQVOtO93NQ


watch() 响应式更改

就像我们在组件中使用 watch 选项或者 $watch api 在 data property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。

  1. watch(source, callback, [options])

参数说明:

  • source:可以支持string,Object,Function,Array; 用于指定要侦听的响应式变量
  • callback: 执行的回调函数
  • options:支持deep、immediate 和 flush 选项。

侦听 reactive 定义的数据

  1. import { defineComponent, ref, reactive, toRefs, watch } from "vue";
  2. export default defineComponent({
  3. setup() {
  4. const state = reactive({ nickname: "xiaofan", age: 20 });
  5. setTimeout(() =>{
  6. state.age++
  7. },1000)
  8. // 修改age值时会触发 watch的回调
  9. watch(
  10. () => state.age,
  11. (curAge, preAge) => {
  12. console.log("新值:", curAge, "老值:", preAge);
  13. }
  14. );
  15. return {
  16. ...toRefs(state)
  17. }
  18. },
  19. });

侦听 ref 定义的数据

  1. const year = ref(0)
  2. setTimeout(() =>{
  3. year.value ++
  4. },1000)
  5. watch(year, (newVal, oldVal) =>{
  6. console.log("新值:", newVal, "老值:", oldVal);
  7. })

侦听多个数据

上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:

  1. watch([() => state.age, year], ([curAge, preAge], [newVal, oldVal]) => {
  2. console.log("新值:", curAge, "老值:", preAge);
  3. console.log("新值:", newVal, "老值:", oldVal);
  4. });

侦听复杂的嵌套对象

我们实际开发中,复杂数据随处可见, 比如:

  1. const state = reactive({
  2. room: {
  3. id: 100,
  4. attrs: {
  5. size: "140平方米",
  6. type:"三室两厅"
  7. },
  8. },
  9. });
  10. watch(() => state.room, (newType, oldType) => {
  11. console.log("新值:", newType, "老值:", oldType);
  12. }, {deep:true});

如果不使用第三个参数deep:true, 是无法监听到数据变化的。

前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。关于flush配置,还在学习,后期会补充

stop 停止监听

我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:

  1. const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
  2. console.log("新值:", newType, "老值:", oldType);
  3. }, {deep:true});
  4. setTimeout(()=>{
  5. // 停止监听
  6. stopWatchRoom()
  7. }, 3000)

watchEffect()

watchEffect 监听器

computed()

与 ref 和 watch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。

  1. import { ref, computed } from 'vue'
  2. const counter = ref(0)
  3. const twiceTheCounter = computed(() => counter.value * 2)
  4. counter.value++
  5. console.log(counter.value) // 1
  6. console.log(twiceTheCounter.value) // 2

vue3 生命周期钩子

可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup () 内部调用生命周期钩子:

vue2选项式 API vue3 Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
  1. 可以看出来vue2的beforeCreatecreated变成了setup
  2. 绝大部分生命周期都是在原本vue2的生命周期上带上了on前缀使用

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

Vue3.x中的钩子是需要从vue中导入的:

这些函数接受一个回调函数,当钩子被组件调用时将会被执行
在setup中使用生命周期:

  1. import { onMounted } from 'vue';
  2. export default {
  3. setup() {
  4. onMounted(() => {
  5. // 在挂载后请求数据
  6. getList();
  7. })
  8. }
  9. };
  1. import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
  2. } from "vue";
  3. export default defineComponent({
  4. // beforeCreate和created是vue2的
  5. beforeCreate() {
  6. console.log("------beforeCreate-----");
  7. },
  8. created() {
  9. console.log("------created-----");
  10. },
  11. setup() {
  12. console.log("------setup-----");
  13. // vue3.x生命周期写在setup中
  14. onBeforeMount(() => {
  15. console.log("------onBeforeMount-----");
  16. });
  17. onMounted(() => {
  18. console.log("------onMounted-----");
  19. });
  20. // 调试哪些数据发生了变化
  21. onRenderTriggered((event) =>{
  22. console.log("------onRenderTriggered-----",event);
  23. })
  24. },
  25. });