使用Vue2.x的小伙伴都熟悉,Vue2.x中所有数据都是定义在data中,方法定义在methods中的,并且使用this来调用对应的数据和方法。那Vue3.x中就可以不这么玩了, 具体怎么玩我们后续再说, 先说一下Vue2.x版本这么写有什么缺陷,所以才会进行升级变更的。
文档
https://vue3js.cn/vue-composition-api/
回顾 Vue2.x 实现加减
<template><div class="homePage"><p>count: {{ count }}</p><p>倍数: {{ multiple }}</p><div><button style="margin-right:10px" @click="increase">加1</button><button @click="decrease">减一</button></div></div></template><script>export default {data() {return {count: 0,};},computed: {multiple() {return 2 * this.count;},},methods: {increase() {this.count++;},decrease() {this.count++;},},};</script>
上面代码只是实现了对count的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:

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

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

那么vue2.x版本给出的解决方案就是Mixin, 但是使用Mixin也会遇到让人苦恼的问题:
- 命名冲突问题
- 不清楚暴露出来的变量的作用
- 逻辑重用到其他 component 经常遇到问题
关于上面经常出现的问题我就不一一举例了,使用过的小伙伴多多少少都会遇到。文章的重点不是Mixin,如果确实想知道的就留言啦~
所以,我们Vue3.x就推出了Composition API主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点认识Composition API。
Composition API
setup()
setup函数是一个新的组件选项。作为在组件内使用**Composition API**的入口点。从生命周期钩子的视角来看,它会在beforeCreate钩子之前被调用,所有变量、方法都在setup函数中定义,之后return出去供外部使用。从setup返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
export default defineComponent ({beforeCreate() {console.log("----beforeCreate----");},created() {console.log("----created----");},setup() {console.log("----setup----");},})

由于在执行setup 时尚未创建组件实例,因此在 setup 选项中没有 this。
你只能访问以下 property:
- props
- attrs
- slots
- emit
换句话说,你将无法访问以下组件选项:
- data
- computed
- methods
该函数有2个参数:
<!-- 组件传值 --><com-setup p1="传值给 com-setup"/>
// 通过 setup 函数的第一个形参,接收 props 数据:setup(props) {console.log(props)},// 在 props 中定义当前组件允许外界传递过来的参数名称:props: {p1: String}
setup中接受的props是响应式的, 当传入新的props 时,会及时被更新。由于是响应式的, 所以不可以使用ES6解构,解构会消除它的响应式。
错误代码示例, 这段代码会让props不再支持响应式:
// demo.vueexport default defineComponent ({setup(props, context) {const { name } = propsconsole.log(name)},})
通过toRefs、toRef解构props
如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作
import { toRefs } from 'vue'setup(props) {const { title } = toRefs(props)console.log(title.value)}
如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:
// MyBook.vueimport { toRef } from 'vue'setup(props) {const title = toRef(props, 'title')console.log(title.value)}
Context
setup 函数的第二个形参 context 是一个上下文对象,前面说了setup中不能访问Vue2中最常用的this对象,所以context中提供属性(attrs,slots,emit,parent,root),其对应于vue2中的this.$attrs,this.$slots,this.$emit,this.$parent,this.$root。
在 vue 3.x 中,它们的访问方式如下:
setup(props, context) {console.log(context)// Attribute (非响应式对象)console.log(context.attrs)// 插槽 (非响应式对象)console.log(context.slots)// 触发事件 (方法)console.log(context.emit)console.log(this) // undefined},/*attrs: Objectemit: ƒ ()listeners: Objectparent: VueComponentrefs: Objectroot: Vue...*/
解构context
export default {setup (props, { emit }) {const handleUpdate = () => {emit('update', 'Hello World')}return { handleUpdate }}}
setup也用作在tsx中返回渲染函数:
setup(props, { attrs, slots }) {return () => {const propsData = { ...attrs, ...props } as any;return <Modal {...propsData}>{extendSlots(slots)}</Modal>;};},
注意:this关键字在setup()函数内部不可用,在方法中访问setup中的变量时,直接访问变量名就可以使用。
为什么props没有被包含在上下文中?
- 组件使用props的场景更多,有时甚至只需要使用props
- 将props独立出来作为一个参数,可以让TypeScript对props单独做类型推导,不会和上下文中其他属性混淆。这也使得setup、render和其他使用了TSX的函数式组件的签名保持一致。
使用渲染函数
setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
import { h, ref, reactive } from 'vue'export default {setup() {const readersNumber = ref(0)const book = reactive({ title: 'Vue 3 Guide' })// Please note that we need to explicitly expose ref value herereturn () => h('div', [readersNumber.value, book.title])}}
reactive(), ref() 创建响应式数据
在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,作用等同于在vue2中的data,不同的是他们使用了ES6的Porxy API解决了vue2 defineProperty 无法监听数组和对象新增属性的痛点,而且使任何响应式变量在任何地方起作用。
<template><div class="contain"><el-button type="primary" @click="numadd">add</el-button><span>{{ `${state.str}-${num}` }}</span></div></template><script lang="ts">import { reactive, ref } from 'vue';interface State {str: string;list: string[];}export default {setup() {const state = reactive<State>({str: 'test',list: [],});//ref需要加上value才能获取const num = ref(1);console.log(num) // { value: 1 }console.log(num.value) // 1const numadd = () => {num.value++;};return { state, numadd, num };},method:{numAdd(){this.num++ //setup return的就是value,所以这里不需要加value}}};</script>
reactive函数可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean等。

上面的代码中,我们绑定到页面是通过user.name,user.age;这样写感觉很繁琐,我们能不能直接将user中的属性解构出来使用呢?答案是不能直接对**user**进行解构, 这样会消除它的响应式, 这里就和上面我们说**props**不能使用ES6直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用**toRefs**。
toRefs()
将传入的reactive对象里所有的属性都转化为响应式数据对象(
ref)
使用reactive return 出去的值每个都需要通过reactive对象 .属性的方式访问非常麻烦,我们可以通过解构赋值的方式范围,但是直接解构的参数不具备响应式,此时可以使用到这个api(也可以对props中的响应式数据做此处理)
将前面的例子作如下👇修改使用起来更加方便:
<template><div class="contain"><el-button type="primary" @click="numadd">add</el-button>- <span>{{ `${state.str}-${num}` }}</span>+ <span>{{ `${str}-${num}` }}</span></div></template><script lang="ts">import { reactive, ref, toRefs } from 'vue';interface State {str: string;list: string[];}export default {setup() {const state = reactive<State>({str: 'test',list: [],});//ref需要加上value才能获取const num = ref(1);const numadd = () => {num.value++;};- return { state, numadd, num };+ return { ...toRefs(state), numadd, num };},};</script>
具体使用方式如下:
toRef()
toRef用来将引用数据类型或者reavtive数据类型中的某个属性转化为响应式数据
reactive 数据类型
/* reactive数据类型 */let obj = reactive({ name: '小黄', sex: '1' });let state = toRef(obj, 'name');state.value = '小红';console.log(obj.name); // 小红console.log(state.value); // 小红obj.name = '小黑';console.log(obj.name); // 小黑console.log(state.value); // 小黑
引用数据类型
<template><span>ref----------{{ state1 }}</span><el-button type="primary" @click="handleClick1">change</el-button><!-- 点击后变成小红 --><span>toRef----------{{ state2 }}</span><el-button type="primary" @click="handleClick2">change</el-button><!-- 点击后还是小黄 --></template><script>import { ref, toRef, reactive } from 'vue';export default {setup() {let obj = { name: '小黄' };const state1 = ref(obj.name); // 通过ref转换const state2 = toRef(obj, 'name'); // 通过toRef转换const handleClick1 = () => {state1.value = '小红';console.log('obj:', obj); // obj:小黄console.log('ref', state1); // ref:小红};const handleClick2 = () => {state2.value = '小红';console.log('obj:', obj); // obj:小红console.log('toRef', state2); // toRef:小红};return { state1, state2, handleClick1, handleClick2 };},};</script>

https://mp.weixin.qq.com/s/avfb-jJeW7f_tQVOtO93NQ
watch() 响应式更改
就像我们在组件中使用 watch 选项或者 $watch api 在 data property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。
watch(source, callback, [options])
参数说明:
- source:可以支持string,Object,Function,Array; 用于指定要侦听的响应式变量
- callback: 执行的回调函数
- options:支持deep、immediate 和 flush 选项。
侦听 reactive 定义的数据
import { defineComponent, ref, reactive, toRefs, watch } from "vue";export default defineComponent({setup() {const state = reactive({ nickname: "xiaofan", age: 20 });setTimeout(() =>{state.age++},1000)// 修改age值时会触发 watch的回调watch(() => state.age,(curAge, preAge) => {console.log("新值:", curAge, "老值:", preAge);});return {...toRefs(state)}},});
侦听 ref 定义的数据
const year = ref(0)setTimeout(() =>{year.value ++},1000)watch(year, (newVal, oldVal) =>{console.log("新值:", newVal, "老值:", oldVal);})
侦听多个数据
上面两个例子中,我们分别使用了两个watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:
watch([() => state.age, year], ([curAge, preAge], [newVal, oldVal]) => {console.log("新值:", curAge, "老值:", preAge);console.log("新值:", newVal, "老值:", oldVal);});
侦听复杂的嵌套对象
我们实际开发中,复杂数据随处可见, 比如:
const state = reactive({room: {id: 100,attrs: {size: "140平方米",type:"三室两厅"},},});watch(() => state.room, (newType, oldType) => {console.log("新值:", newType, "老值:", oldType);}, {deep:true});
如果不使用第三个参数deep:true, 是无法监听到数据变化的。
前面我们提到,默认情况下,watch是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置immediate: true即可。关于flush配置,还在学习,后期会补充
stop 停止监听
我们在组件中创建的watch监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用watch()函数的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {console.log("新值:", newType, "老值:", oldType);}, {deep:true});setTimeout(()=>{// 停止监听stopWatchRoom()}, 3000)
watchEffect()
computed()
与 ref 和 watch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。
import { ref, computed } from 'vue'const counter = ref(0)const twiceTheCounter = computed(() => counter.value * 2)counter.value++console.log(counter.value) // 1console.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 |
- 可以看出来vue2的
beforeCreate和created变成了setup - 绝大部分生命周期都是在原本vue2的生命周期上带上了
on前缀使用
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。
Vue3.x中的钩子是需要从vue中导入的:
这些函数接受一个回调函数,当钩子被组件调用时将会被执行
在setup中使用生命周期:
import { onMounted } from 'vue';export default {setup() {onMounted(() => {// 在挂载后请求数据getList();})}};
import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered} from "vue";export default defineComponent({// beforeCreate和created是vue2的beforeCreate() {console.log("------beforeCreate-----");},created() {console.log("------created-----");},setup() {console.log("------setup-----");// vue3.x生命周期写在setup中onBeforeMount(() => {console.log("------onBeforeMount-----");});onMounted(() => {console.log("------onMounted-----");});// 调试哪些数据发生了变化onRenderTriggered((event) =>{console.log("------onRenderTriggered-----",event);})},});
