TOC

Composition API

    在此前,我们学习了很多的Options API,那么在Vue 3.x版本中,新增了一个Composition API,可以直接使用它来替代原来Options API里面的很多功能;
    没有Composition API之前,业务相关的代码需要配置到组件对象内中,作为Options API,中小型项目是没有问题的,但是在大型项目中会导致后期的维护性比较复杂,同时代码可复用性不高,那么Vue3.x中的Composition API就是为了解决这个问题而生的,Composition API是Vue 3.x中最常用的讨论和特色语法,它是一种全新的逻辑重用和代码组织方法当前;
    传统Options API配置方法写组件的时候,随着业务复杂度越来越高,代码量会越来越大,由于相关业务的代码需要写在data、methods等位置,后续维护复杂,且代码可复用性不高,那么使用使用Composition API就能很好的规避这些问题,Composition API的好处很多,代码都在一个setup()里面,这样它更加灵活,不受条条框框约束,增加代码可复用性;
    其实在Vue3的创作初期,Vue作者依然是采用的Options API开发,但是进过社区的反馈和实际的实验,发现Options API在一些情况下会造成问题,当项目庞大时,无法将代码进行抽离和调度,虽然在Vue2中提供了Mixins这样的Options但是大量的Mixins的使用会造成代码的混乱和难以维护性,因此Vue作者经过多方考虑才接入来composition api的涉及思想;

Setup函数

    Setup函数是Composition API的入口函数,变量、方法都在该函数中定义,Setup函数是先于beforeCreate和created执行的,由于在执行Setup函数时尚未创建组件实例,因此在Setup函数没有this;
    在Vue 3.x中,我们主要是使用setup函数编写Composition API,Setup函数其实就是组件对象的的另外一个选项,说白了它还是一个Options API,只不过这个Options API强大到我们可以使用它来替代之前所编写的大部分其他Options API,比如,methods、computed、data、生命周期等Options API;
Setup函数参数
    Setup函数,默认会接收两个参数,第一个为props,默认会传递过来props,只要父组件向当前组件传递过来了数据,就可以通过这个props参数获取父组件传递过来的数据,当然,如果我们想要去定义一些props属性的话,和之前一样,但是如果我们需要在setup函数中拿到props中的某个值,不可以使用this去拿,而是通过这个参数去获取;
    第二个为context,即上下文,它是一个对象,这个对象里面包含三个属性,第一个为attrs,表示所有非prop的attribute,第二个为slots,表示父组件传递过来的插槽,第三个是emit,即自定义事件,如果我们希望使用自定义事件,直接使用这个context对象里面的emit属性即可;
Setup函数返回值-数据
    在Setup函数中,如果我们希望使用Mustache语法,在模版使用原来data函数返回的数据,那么Setup函数必须有返回值,且这个返回值是一个对象,将需要使用Mustache语法在模版使用的数据,放到这个对象中,如下;
<template>
  <div>
    <p>{{ counter }}</p>
  </div>
</template>

<script>
export default {
  setup(prop, context) {
    let counter = 100;
    return {
      counter // ES6对象简写形式
    }
  }
}
</script>

Setup函数返回值-函数
    可以看到在使用了Composition API之后,和原来的Vue 2.x语法的变化非常大,如果希望开发之前methods函数里面的事件处理方法,那么在接入Composition API之后,直接写在Setup函数中即可,同样的,将它作为Setup函数返回的对象内的一个属性即可,如下;
<template>
  <div>
    <button v-on:click="add">add</button>
  </div>
</template>

<script>
export default {
  setup(prop, context) {
    let add = () => console.log(1)
    return {
      add
    }
  }
}
</script>

Ref函数

    虽然Setup函数中,我们可以直接返回对象,且这个对象内的属性可以在模版中使用Mustache语法引用,但是有一个问题,就是这个数据并非是一个响应式数据,换而言之,当数据发生改变时,页面不会根据数据的改变而重新渲染;
    那么解决这个问题就需要引入Vue对象中的一个ref函数,它主要用途就是创建一个响应式数据,这个响应式对象会被Vue响应式系统劫持,一旦出现数据的变动就会采取响应的处理;
    如果我们希望使一个数据是一个响应式数据,那么我们可以将这个数据直接作为参数传递给这个ref函数,它会直接返回一个响应式对象,这个对象中的一个value属性就是,我们需要的数据,所以,如果我们在引用这个响应式数据时,就需要使用这个响应式对象的value属性来获取;
    但是,这里需要注意的是,如果我们在模版中引用,不需要使用这个响应式对象的value属性来获取这个值,因为默认情况下,在模版中使用ref响应式对象,Vue模版引擎会自动对其进行解包,会自动取出其中的value,所以在模版中,我们无需调用这个响应式对象的value属性来获取值;
<template>
  <div>
    <p>{{ counter }}</p>
    <button v-on:click="add">add</button>
  </div>
</template>

<script>
import {ref} from 'vue'  // 引入ref函数

export default {
  setup(prop, context) {
    let counter = ref(0);
    function add() {  // 可以换成箭头函数
      counter.value++
    }
    return {
      counter,
      add
    }
  }
}
</script>

Reactive函数

    Reactive函数和Ref函数类似,但是Ref函数不推荐包装复杂的数据类型,如数组、对象,所以Vue框架提供了一个Reactive函数,但是它对传入的数据有类型限制,它要求我们必须传入一个对象或者数组类型,如果我们传入一个基本的数据类型,如String、Number、Boolean等,它会抛出一个警告;
    Reactive函数的使用方法和Ref函数一致,直接将我们的复杂类型的数据传递给Reactive函数即可,它会返回一个响应式对象,唯一的差别就是,我们在setup函数内引用这个数据时,不需要调用这个响应式对象的value属性,如下示例;
<template>
  <div>
    <p>{{ names }}</p>
    <button v-on:click="pop">pop</button>
  </div>
</template>

<script>
import {reactive} from 'vue' // 引入reactive函数

export default {
  setup(prop, context) {
    let names = reactive([1, 2, 3, 4]);
    function pop() {  // 可以换成箭头函数
      names.pop()
    }
    return {
      names,
      pop
    }
  }
}
</script>

ReadOnly函数

    在Vue中,有一个单向数据流的概念,它指的是,当父子数据共享时,父组件只负责给子组件传递数据,子组件收到数据后,不推荐修改,如果确实需要修改,那么建议通过子传父的形式,让父组件来修改,这就称之为单向数据流;
    但是在实际应用场景中,并非是这样的,比如我们父组件,给子组件传递了一个对象,当子组件拿到这个对象之后是可以对这个对象进行修改操作的,因为父组件传过来的只是一个对象的引用,说白了,此时的父子组件使用的是内存中同一块数据,那么这个时候子组件就完全可以随意修改这个数据了;
    那么为了避免这种问题的出现,Vue 3.x提供了一个ReadOnly函数,它和Ref类似,它可以接收一个数组、对象同时它也可以接收一个Ref函数或者Reactive函数返回的响应式对象,同时它创建出来的数据也是响应式的,但是它不允许对这个响应式数据进行修改,如下示例;
<!--父组件-->
<template>
  <div>
    <component_1 :infos="readOnlyInfos"></component_1>
  </div>
</template>

<script>
import {ref, readonly} from 'vue' // 引入reactive函数
import component_1 from './components/component_1.vue' // 引入reactive函数

export default {
  setup(prop, context) {
    let infos = ref([1, 2, 3, 4]);
    let readOnlyInfos = readonly(infos)
    return {
      readOnlyInfos
    }
  },
  components: {
    component_1
  }
}
</script>

<!--子组件-->
<template>
  <div>
    <p>当前值为:{{ infos }}</p>
    <button v-on:click="infos.pop()">修改数据</button>
  </div>
</template>

<script>
export default {
  props: ["infos"]
}
</script>

子传父
    所以,此时,我们应该通过子传父的方式,将这个数据修改操作传递给父组件,由父组件来进行数据的修改,如下示例;
<!--父组件-->
<template>
  <div>
    <component_1 :infos="readOnlyInfos" v-on:parentEdit="Edit"></component_1> <!-- 实现父传子,并监听组件自定义事件 -->
  </div>
</template>

<script>
import {reactive, ref, readonly} from 'vue' // 引入reactive函数
import component_1 from './components/component_1.vue' // 引入reactive函数

export default {
  setup(prop, context) {
    let infos = ref([1, 2, 3, 4]);
    let readOnlyInfos = readonly(infos)

    function Edit() {
      infos.value.pop()  // 直接修改ref对象
    }

    return {
      readOnlyInfos,
      Edit
    }
  },
  components: {
    component_1
  }
}
</script>

<!--子组件-->
<template>
  <div>
    <p>当前值为:{{ infos }}</p>
    <button v-on:click="parentEdit">修改数据</button>
  </div>
</template>

<script>
export default {
  props: ["infos"],
  setup(props, context) {
    function parentEdit() {
      context.emit("parentEdit")  // 触发自定义事件
    }

    return {
      parentEdit
    }
  }
}
</script>

其他函数

    Vue框架除了提供了Ref、Reactive和ReadOnly函数之外,还提供了其他的函数,稍微常用的几个如下;
isProxy:检查对象是否由Reactive或者ReadOnly创建的Proxy对象;
isReactive:检查对象是否由Reactive函数创建的响应式对象,如果对象由Reactive函数创建,但是被ReadOnly包裹一样为true;
isReadOnly:检查对象是否由ReadOnly函数创建;
toRaw:返回reactive或者readonly代理的原始对象;
shallowReactive:创建一个响应式代理,它跟踪其自身property的响应性;

computed函数

    computed计算属性,当我们从Options API转到Composition API之后,计算属性的写法其实也有相应的改变,我们可以直接在setup里面,使用conputed函数即可,这个函数是vue对象当中的一个属性,该函数的参数可以是一个函数也可以是一个对象,我们直接将计算结果在这个函数中返回即可,如下;
<template>
  <div>
    <p>{{ fullName }}</p>
  </div>
</template>

<script>
import {ref, computed} from 'vue' // 引入computed函数

export default {
  setup(props, context) {
    let firstName = ref("Cai");
    let lastName = ref("ChangEn");
    let fullName = computed(() => { // 定义计算属性
      return firstName.value + lastName.value
    })
    return {
      fullName
    }
  }
}
</script>

computed高级
    当我们在computed函数内部传入一个函数时,默认是一个getter的方法,如果我们希望同时设置getter和setter方法,那么就需要给computed函数,传递一个对象,这个对象内部具有get和set两个函数,其中set方法接收一个参数,即传入的新值;
    但是这里需要注意的是,computed函数返回的其实是一个ref对象,所以,当我们需要拿到一个通过computed计算出来的值时,需要通过这个对象的value属性来获取;
<template>
  <div>
    <p>{{ fullName }}</p>
    <button v-on:click="edit">修改fullName</button>
  </div>
</template>

<script>
import {ref, computed} from 'vue' // 引入computed函数

export default {
  setup(props, context) {
    let firstName = ref("Cai");
    let lastName = ref("ChangEn");
    let fullName = computed({
      get: function () {  // 设置getter属性
        return firstName.value + lastName.value
      },
      set: function (newValue) {  // 设置setter属性,默认接收一个参数
        let [fName, lName] = newValue.split(' ')
        firstName.value = fName
        lastName.value = lName
      }
    })
    let edit = function () {  // 修改计算属性
      fullName.value = 'Cai FengJun'  // 调用computed返回值的value属性拿到值
    }
    return {
      fullName,
      edit
    }
  }
}
</script>

元素选择器

    因为在Setup函数中并没有this,所以如果我们希望像Options API那样使用this.$ref.ref_name的形式去获取DOM当中的元素是不可行的,那么在Vue3中,如果我们希望拿到的指定ref的元素,就需要用到ref函数,这是ref函数的另一个作用,当它创建的响应式对象,被赋予到一个元素的ref属性上面时,这个响应式对象的value值,就是元素本身,但是这里需要注意的是,在DOM当中,不可以使用v-bind来绑定ref属性,直接将这个响应式对象作为一个字符串赋值给元素的ref属性即可;
    同时,因为Setup函数在create生命周期之前,所以,我们无法在Setup函数中直接获取元素,因为此时DOM还没有形成,所以,我们需要加入mounted生命周期,在Composition API中,如果想要使用生命周期函数,直接在Vue对象中去拿即可,如下;
<!--父组件-->
<template>
  <div>
    <h1 ref="titleRef">h1</h1>
  </div>
</template>

<script>
import {ref, onMounted} from 'vue'

export default {
  setup() {
    let titleRef = ref('cce');
    onMounted(() => console.log(titleRef.value))
    return {
      titleRef
    }
  }
}
</script>

  • 提示:其实这里为什么是这样设计的,也没太搞懂,为什么它能直接拿到元素?为什么不需要使用v-bind语法?感觉很鸡肋;

生命周期函数

    在Composition API中,生命周期的函数也不再是之前Options API那样去编写了,首先Composition API当中的Setup函数本身就替代了data、methods、computed等选项,同时,它也可以替代生命周期函数,但是在Composition API中默认取消了两个生命周期函数,即beforeCreate和created,因为它就直接被Setup函数替代了,直接在Setup函数中去编写相应的逻辑即可;
    那么如果我们希望在Setup函数中使用生命周期函数,只需要在Setup函数当中注册生命周期函数即可,这些注册生命周期的函数都在Vue对象内,我们可以直接使用import语法导入,但是它的名称就有点特殊了,以"on"开头,如原来的mounted生命周期钩子,现在如果我们使用import语法在Vue对象中去导入名称需要更换为onMounted,那么对于Composition API中,我们可以注册如下几种类型的生命周期,以及其对应的生命周期函数;
Options API
Composition API
beforeMount
onBeforeMount
mounted
onMounted
beforeUpdate
onBeforeUpdate
update
onUpdate
beforeUnmount
onBeforeUnmount
unmounted
onUnmounted
activated
onActivated
deactivated
onDeactivated
    简单的测试beforeMount和mounted两个生命周期,如下示例;
<template>
  <div>
  </div>
</template>

<script>
import {onBeforeMount, onMounted} from 'vue' // 导入注册函数

export default {
  setup() {
    onBeforeMount(() => console.log('beforeMount')) // 注册beforeMount生命周期
    onMounted(() => console.log('mounted')) // 注册mounted生命周期
  }
}
</script>

Provide/Inject组件间通信

    对于Provide/Inject组件间通信来讲,在Options API中,是通过组件对象的属性的方式来实现的,那么在Composition API当中,也进行了变更,主要是通过两个函数来实现,祖先组件间的通信,一个是provide函数,通过provide函数来给子、子孙组件暴露数据,另一个是inject函数,子组件通过inject函数来获取数据;
    此外,inject函数是可以接收两个参数的,第一个参数为获取祖先组件暴露出来的哪个数据,第二个参数为默认值,当祖先组件中并未传递该数据时,使用默认值代替;
<!--父组件-->
<template>
  <div>
    <component_1></component_1>
  </div>
</template>

<script>
import component_1 from './components/component_1.vue'
import {provide} from 'vue' // 导入provide
export default {
  components: {
    component_1
  },
  setup() {
    provide("name", "cce") // 向子孙组件暴露数据
    provide("age", 18)
  }
}
</script>

<!--子组件-->
<template>
  <div>
    <p>{{ name }},{{ age }}</p>
  </div>
</template>

<script>
import {inject} from 'vue' // 导入inject
export default {
  setup() {
    let name = inject('name', undefined)
    let age = inject('age', undefined)
    return {
      name,
      age
    }
  }
}
</script>

响应式处理
    这种方式向子组件暴露的数据,并非响应式数据,如果我们希望这个数据是响应式的,那么需要将这个数据先用ref包裹一层,但是这里需要注意的是,如果我们使用ref手动暴露了一层,且在子组件中使用Options API的方式来获取这个数据,那么在子组件中的模版语言中就需要手动解包,即,调用这个ref对象的value属性;
    所以这里还是建议直接在Setup函数中使用inject函数来获取这个值,这样就不需要在Mustache语法中使用{{ attr.value }}的方式来获取数据了,但是在Setup函数中想要拿到数据,依旧得使用value属性,如下;
<!--父组件-->
<template>
  <div>
    <component_1></component_1>
  </div>
</template>

<script>
import component_1 from './components/component_1.vue'
import {provide,ref} from 'vue' // 导入provide
export default {
  components: {
    component_1
  },
  setup() {
    provide("name", ref("cce")) // 向子孙组件暴露数据
    provide("age", ref(18))
  }
}
</script>

<!--子组件-->
<template>
  <div>
    <p>{{ name }},{{ age }}</p>
  </div>
</template>

<script>
import {inject} from 'vue' // 导入inject
export default {
  setup() {
    let name = inject('name', undefined)
    let age = inject('age', undefined)
    console.log(name.value, age.value)
    return {
      name, age
    }
  }
}
</script>

watch侦听器

    在此之前想要侦听数据的变化是通过Options API的方式来实现的,那么在Vue 3的Composition API中,watch侦听器和上述的计算属性、生命周期等一样,变成了一个函数,我们可以直接在Vue对象中使用import语法导入这个函数,然后直接在Setup函数中使用;
    该函数默认接收三个参数,第一个参数为侦听的变量标识符,第二个参数为侦听器函数,第三个参数为一个对象,里面有两个属性,即immediate和deep,如下示例;
<!--父组件-->
<template>
  <div>
    <p>{{ name }}</p>
    <button v-on:click="name = 'cfj'">修改数据</button>
  </div>
</template>

<script>
import {watch, ref} from 'vue' // 导入watch
export default {
  setup() {
    let info = ref({
      name: "cce",
      age: 18
    });
    watch(info, (newValue, oldValue) => {
      console.log(newValue, oldValue)
    }, {
      immediate: true, // 页面打开的那一刻,调用一次侦听器函数
      deep: false // 深度监听
    })
    return {
      info
    }
  }
}
</script>

Setup函数语法糖

    可以看到,当我们切换到Composition API之后,大部分的代码逻辑都放在了Setup函数中,只有一些非常特殊功能才会去单独编写Options API,比如props、components以及emits,其他所有的东西都可以写到Setup函数中,所以Vue就对Setup函数提供了一种语法糖,这个语法糖在Vue 3.2版本才正式发布;
    如果所有的逻辑都打算写到Setup函数中,那么可以直接在<script>元素上加入一个setup属性即可,如下示例;
<script setup>
// setup函数内的逻辑
</script>
    这只不过是一种语法糖的写法,在进行编译的时候还是会将其编译成Setup函数的形式,并自动将Setup函数语法糖内部的一些变量标识符以及import的对象暴露出来,也就是说明,我们可以直接在模版中使用Mustache语法引用Setup语法糖内部的标识符,如下;
<template>
  <div>
    <p>{{ name }}</p>
  </div>
</template>

<script setup>
let name = 'cce'
</script>

components
    当我们使用了Setup函数语法糖之后,就没有办法编写Options API了,所以,我们的App.vue也没有办法再使用components语法来引入别的组件了,因此在Setup语法糖内,如果我们希望引入组件,非常的简单,我们只需要在setup语法糖内import导入一下这个组件即可,Vue框架在编译时会自动将这个import后的组件对象,注册到当前组件当中,如下示例;
<!--父组件-->
<template>
  <div>
    <component_1></component_1>
  </div>
</template>

<script setup>
import component_1 from "./components/component_1.vue"
</script>

<!--子组件-->
<template>
  <div>
    <h1>子组件</h1>
  </div>
</template>

props和emits
    同时对于props和emits这两类属性也不再支持,那么Vue专门为它们提供了两个函数,即defineEmits和defineProps函数,我们可以直接在Setup函数语法糖内使用一个这两个函数来实现,父子消息通信;
    对于defineProps函数来讲,它有一个参数,并且该参数是一个对象,在这个对象内,我们可以编写具体需要获取父组件中传来的数据;
    而defineEmits函数则有点不一样,它可以接收一个参数,这个参数是可省的,该参数只是让我们能清楚的看到,该组件会向父亲组件传递哪些自定义事件,只是让开发者在代码逻辑较为复杂时能够清晰的看到具体触发了哪些自定义事件,然后该函数返回一个自定义事件的对象,我们可以直接使用这个自定义事件的对象来代替原来的this.$emit对象,用这个自定义事件的对象来触发自定义事件,如下示例;
<!--父组件-->
<template>
  <div>
    <component_1 :info="user_info" v-on:showclick="custom_event"></component_1> <!-- 监听自定义事件,并向子组件传递数据 -->
  </div>
</template>

<script setup>
import component_1 from "./components/component_1.vue"
import {ref} from "vue"

let user_info = ref({
  name: "cce",
  age: 18
})

function custom_event(arg) {
  console.log(arg)
}
</script>

<!--子组件-->
<template>
  <div>
    <p>{{ info.name }} {{ info.age }}</p>
    <button v-on:click="showClick">触发事件</button>
  </div>
</template>

<script setup>
defineProps({  // 接收来自父组件的数据
  info: {
    type: Object,
    default: {}
  }
})
let emit = defineEmits() // 获取自定义事件对象

function showClick() {
  emit("showclick", "子组件发生点击事件");  // 触发自定义事件
}
</script>

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注