前端Vue13.Vue3组合式API
墨颜丶Vue3 组合式 API(Composition API) 主要用于在大型组件中提高代码逻辑的可复用性。
传统的组件随着业务复杂度越来越高,代码量会不断的加大,整个代码逻辑都不易阅读和理解。
Vue3 使用组合式 API 的地方为 setup。
在 setup 中,我们可以按逻辑关注点对部分代码进行分组,然后提取逻辑片段并与其他组件共享代码。因此,组合式 API(Composition API) 允许我们编写更有条理的代码。
对比以下两端代码:
1、传统组件

2、组合式 API

响应式基础
ref()
声明响应式状态
ref() 函数可以根据给定的值来创建一个响应式的数据对象,返回值是一个对象,且只包含一个 .value 属性。
在 setup() 函数内,由 ref() 创建的响应式数据返回的是对象,所以需要用 .value 来访问。
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:
1 2 3
| import { ref } from 'vue'
const count = ref(0)
|
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
1 2 3 4 5 6 7
| const count = ref(0)
console.log(count) console.log(count.value)
count.value++ console.log(count.value)
|
要在组件模板中访问 ref,请从组件的 setup()
函数中声明并返回它们:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { ref } from 'vue'
export default { setup() { const count = ref(0)
return { count } } }
|
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
你也可以直接在事件监听器中改变一个 ref:
1 2 3
| <button @click="count++"> {{ count }} </button>
|
setup()
setup()
钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
对于结合单文件组件使用的组合式 API,推荐通过 script setup 以获得更加简洁及符合人体工程学的语法。
我们可以使用响应式 API 来声明响应式的状态,在 setup()
函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup()
暴露的属性:
setup() 函数在组件创建 created() 之前执行。
setup() 函数接收两个参数 props 和 context。
第一个参数 props,它是响应式的,当传入新的 prop 时,它将被更新。
第二个参数 context 是一个普通的 JavaScript 对象,它是一个上下文对象,暴露了其它可能在 setup 中有用的值。
**注意:**在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。
以下实例使用组合 API 定义一个计数器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <div> <p>计数器实例: {{ count }}</p> <input @click="myFn" type="button" value="点我加 1"> </div> </template>
<script> import {ref, onMounted} from 'vue';
export default { setup(){ let count = ref(0);
function myFn(){ console.log(count); count.value += 1; } onMounted(() => console.log('component mounted!'));
return {count,myFn} } } </script>
|
<script setup>
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的<script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
在 setup()
函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup>
来大幅度地简化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <button @click="increment"> {{ count }} </button> </template>
<script setup> import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ } </script>
|
reactive()
- reactive同样为我们的值创建了一个响应式引用
- 定义基本普通类型数据
不能用reactive
,用ref - reactive主要定义复杂数据类型,比如数组,对象
- reactive可响应深层次的数据,比如多维数组
- reactive返回一 个响应式的proxy对象
- 使用需引入
与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性:
1 2 3
| import { reactive } from 'vue'
const state = reactive({ count: 0 })
|
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <template> <div> <h1>姓名:{{ name }}</h1> <h1>年龄:{{ obj.age }}</h1> <h1>数据:{{ obj.pro.a.b[0] }}</h1> <button @click="btn">点击我更新数据</button> </div> </template>
<script setup> import { reactive } from 'vue'; const name = reactive('马云') const age = 50 const obj = reactive({ age, pro: { a: { b: ['我是深层次的数据'] } } }) function btn() { console.log(obj); obj.age = 80 obj.pro.a.b[0] = "我被修改了" } </script>
|
reactive()
API 有一些局限性:
有限的值类型:它只能用于对象类型 (对象、数组和如 Map
、Set
这样的集合类型)。它不能持有如 string
、number
或 boolean
这样的原始类型。
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
1 2 3 4 5
| let state = reactive({ count: 0 })
state = reactive({ count: 1 })
|
对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
1 2 3 4 5 6 7 8 9 10 11
| const state = reactive({ count: 0 })
let { count } = state
count++
callSomeFunction(state.count)
|
由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API。
toRef()
- toRef也可以创建一个响应式数据
- ref本质是拷贝粘贴一份数据,脱离了与原数据的交互
- ref函数将对象中的属性变成响应式数据,修改响应式数据是不会影响到原数据,但是会更新视图层
- toRef的本质是引用,与原始数据有交互,修改响应式数据会影响到原数据,但是不会更新视图层
- 使用需引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div> <h1>姓名:{{ obj.name }}</h1> <h1>年龄:{{ obj.age }}</h1> <button @click="btn">点击我更新数据</button> </div> </template>
<script setup> import { toRef } from 'vue'; const obj = {name:'马云',age:50} const res = toRef(obj,'name') function btn() { console.log(obj); res.value='马化腾' } </script>
|

toRefs()
- 用于批量设置多个数据为响应式数据
- toRefs与原始数据有交互,修改响应式数据会影响到原数据,但是不会更新视图层
- toRefs还可以与其他响应式函数交互,更加方便处理视图层数据
- 使用需引入
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <div> <h1>姓名:{{ name }}</h1> <h1>年龄:{{ age }}</h1> <h1>a:{{ a }}</h1> <h1>b:{{ b }}</h1> <h1>c:{{ c }}</h1> <button @click="updateData">点击我更新数据</button> </div> </template> <script> import { reactive, toRefs } from 'vue'; export default { setup() { const obj = reactive({name: '马云', age: 50, a: 1, b: 2, c: 3}); const updateData = () => { console.log('Data updated!'); }; return { ...toRefs(obj), updateData }; } } </script>
|

计算属性
computed
- 用于声明要在组件实例上暴露的计算属性。
- 与Vue2一致,均是用来监听数据变化
- 使用需引入
以下是利用computed
常用的两种方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <template> <div> 计算器:<input type="number" v-model="num1"> + <input type="number" v-model="num2"> = <input type="number" v-model="num"> <br> 文本拼接:<input type="text" v-model="t1"> + <input type="text" v-model="t2"> = <input type="text" v-model="t"> </div> </template> <script> import { ref,computed,reactive,toRefs } from 'vue'; export default { setup() { let num1 = ref() let num2 = ref()
const num = computed(()=>{ return num1.value+num2.value })
const t1='' const t2='' const obj = reactive({t1,t2}) const t = computed(()=>{ return obj.t1+'+'+obj.t2 })
return{num1,num2,num,...toRefs(obj),t}
} } </script>
|
侦听器
watch
用于声明在数据更改时调用的侦听回调。
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- …或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()
。onTrack / onTrigger
:调试侦听器的依赖。参考调试侦听器。
与 watchEffect()
相比,watch()
使我们可以:
- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
示例:
- 侦听一个 ref:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> <h1>{{ p1 }}</h1> <button @click="p1++">点击我+1</button> </div> </template> <script> import { ref,watch } from 'vue'; export default { setup() { const p1=ref(0) watch(p1,(newVal,oldVal)=>{ console.log("新数据",newVal,"旧数据",oldVal); }) return{p1,} } } </script>
|
1 2 3 4 5 6
| 新数据 1 旧数据 0 新数据 2 旧数据 1 新数据 3 旧数据 2 新数据 4 旧数据 3 新数据 5 旧数据 4 新数据 6 旧数据 5
|
- 侦听多个数据变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { ref, watch } from 'vue';
export default { setup() { const p1 = ref(0) const p2 = ref(0) const p3 = ref(0) watch(p1, (newVal, oldVal) => { console.log("新数据", newVal, "旧数据", oldVal); }) watch(p2, (newVal, oldVal) => { console.log("新数据", newVal, "旧数据", oldVal); }) watch(p3, (newVal, oldVal) => { console.log("新数据", newVal, "旧数据", oldVal); }) return { p1,p2,p3 } } }
|
简便写法:监听一个数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { ref, watch } from 'vue';
export default { setup() { const p1 = ref(0) const p2 = ref(0) const p3 = ref(0) watch([p1,p2,p3], (newVal, oldVal) => { console.log("新数据", newVal, "旧数据", oldVal); }) return { p1,p2,p3 } } }
|
- 侦听整个对象数据变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <div> <h1>马云年龄:{{ obj.age }}</h1> <button @click="obj.age++">点击我马云年龄+1</button> </div> </template> <script> import { watch, reactive } from 'vue';
export default { setup() { const obj = reactive({ name: "马云", age: 50 }) watch(obj, (newVal, oldVal) => { console.log("新数据", newVal, "旧数据", oldVal); }) return { obj } } } </script>
只能监听到最新的结果,上一个的数据是监听不到的 新数据 Proxy(Object) {name: '马云', age: 51} 旧数据 Proxy(Object) {name: '马云', age: 51} 新数据 Proxy(Object) {name: '马云', age: 52} 旧数据 Proxy(Object) {name: '马云', age: 52} 新数据 Proxy(Object) {name: '马云', age: 53} 旧数据 Proxy(Object) {name: '马云', age: 53}
|
- 侦听对象数据某一个数据变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <div> <h1>马云年龄:{{ obj.age.num }}</h1> <button @click="obj.age.num++">点击我马云年龄+1</button> </div> </template> <script> import { watch, reactive } from 'vue';
export default { setup() { const obj = reactive({ name: "马云", age: { num:50 } }) watch(()=>obj.age.num, (newVal, oldVal) => { console.log("新数据", newVal, "旧数据", oldVal); },{immediate:true}) return { obj } } } </script>
新数据 50 旧数据 undefined 新数据 51 旧数据 50 新数据 52 旧数据 51 新数据 53 旧数据 52
|
watchEffect()
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
watchEffect如果存在的话,在组件初始化的时候就会执行一次用以收集依赖
watch可以获取到新值与旧值(更新前的值),而watchEffect是拿不到的
watchEffect不需要指定监听的属性,他会自动的收集依赖,只要我们回调中引用到了响应式的属性,
那么当这些属性变更的时候,这个回调都会执行,而watch只能监听指定的属性而做出变更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <div> <h1>马云年龄:{{ obj.age.num }}</h1> <button @click="obj.age.num++">点击我马云年龄+1</button> </div> </template> <script> import { watchEffect, reactive } from 'vue';
export default { setup() { const obj = reactive({ name: "马云", age: { num:50 } }) watchEffect(()=>{ console.log(obj.age.num); }) return { obj } } } </script>
|
停止侦听
1 2 3 4
| const res = watchEffect(()=>{ console.log(obj.age.num); }) res()
|
响应式进阶
shallowRef()
只能处理基本类型数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <div> <h1>姓名:{{ obj.name }}</h1> <h1>年龄:{{ obj.age.num }}</h1> <button @click="obj.age.num++">点击我马云年龄+1</button> <br> <h1>姓名:{{ obj1 }}</h1> <h1>年龄:{{ obj2 }}</h1> <button @click="obj2++">点击我马化腾年龄+1</button> </div> </template> <script> import { shallowRef } from 'vue';
export default { setup() { const obj = shallowRef({ name:'马云', age:{ num:50 } }) const obj1 = shallowRef('马化腾') const obj2 = shallowRef(50) return { obj,obj1,obj2 } } } </script>
|

shallowReactive()
只处理第一层数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <div> <h1>姓名:{{ obj.name }}</h1> <h1>年龄:{{ obj.age.num }}</h1> <button @click="obj.name+='1'">点击我马云姓名+1</button> <button @click="obj.age.num++">点击我马云年龄+1</button> </div> </template>
<script> import { shallowReactive } from "vue";
export default { setup() { const obj = shallowReactive({ name: "马云", age: { num: 50, }, }); return { obj }; }, }; </script>
|

组件间传值
provide()
提供一个值,可以被后代组件注入。
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
与注册生命周期钩子的 API 类似,provide()
必须在组件的 setup()
阶段同步调用。
示例
在App.vue
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <script> import HelloWorld from './components/HelloWorld.vue' import {reactive,provide} from 'vue' export default{ name:'App', components:{ HelloWorld }, setup(){ const p1 = reactive({name:'马云',age:50}) provide('aaa',p1) return{p1} } } </script>
<template> <div class="back"> 我是父组件 <h3>{{ p1.name }}</h3> <h3>{{ p1.age }}</h3> </div> <HelloWorld/> </template>
<style> .back{ background-color: darkkhaki; padding: 20px 0; } </style>
|
inject()
注入一个由祖先组件或整个应用 (通过 app.provide()
) 提供的值。
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject()
将返回 undefined
,除非提供了一个默认值。
第二个参数是可选的,即在没有匹配到 key 时使用的默认值。
第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,你必须将 true
作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。
与注册生命周期钩子的 API 类似,inject()
必须在组件的 setup()
阶段同步调用。
示例
在src\components\HelloWorld.vue
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script> import { inject } from 'vue' export default{ setup() { const p1 = inject('aaa') const p2 = inject('bbb',"接收为空默认值") console.log(p1); return{p1,p2} } } </script>
<template> <div class="back"> 这是子组件 <h2>{{ p1.name }}</h2> <h2>{{ p1.age }}</h2> <h2>{{ p2 }}</h2> </div> </template>
<style scoped> .back{ background-color: brown; color: cyan; padding: 20px 0; } </style>
|

点击按钮后再进行传值
在App.vue
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <script> import HelloWorld from './components/HelloWorld.vue' import {reactive,ref} from 'vue' export default{ name:'App', components:{ HelloWorld }, setup(){ const val = ref() const p1 = reactive({name:'马云',age:50}) function btn(){ val.value.qqq(p1) } return{p1,btn,val} } } </script>
<template> <div class="back"> 我是父组件 <h3>{{ p1.name }}</h3> <h3>{{ p1.age }}</h3> <button @click="btn">给子组件传值</button> </div> <HelloWorld ref="val"/> </template>
<style> .back{ background-color: darkkhaki; padding: 20px 0; } </style>
|
在src\components\HelloWorld.vue
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <script> import { ref } from 'vue' export default{ setup() { const p1 = ref('') function qqq(val){ console.log('我被父组件调用了') p1.value = val } return{qqq,p1} } } </script>
<template> <div class="back"> 这是子组件 <h2>{{ p1.name }}</h2> <h2>{{ p1.age }}</h2> </div> </template>
<style scoped> .back{ background-color: brown; color: cyan; padding: 20px 0; } </style>
|

生命周期钩子
在 Vue2 中,我们通过以下方式实现生命周期钩子函数:
1 2 3 4 5 6 7 8
| export default { beforeMount() { console.log('V2 beforeMount!') }, mounted() { console.log('V2 mounted!') } };
|
在 Vue3 组合 API 中实现生命周期钩子函数可以在 setup() 函数中使用带有 on 前缀的函数:
1 2 3 4 5 6 7 8 9 10 11
| import { onBeforeMount, onMounted } from 'vue'; export default { setup() { onBeforeMount(() => { console.log('V3 beforeMount!'); }) onMounted(() => { console.log('V3 mounted!'); }) } };
|
下表为 Options API
和 Composition API
之间的映射,包含如何在 setup ()
内部调用生命周期钩子:
Vue2 Options-based API | Vue Composition API | |
---|
beforeCreate | setup() | 在实例创建之前调用 |
created | setup() | 实例创建完毕后调用 |
beforeMount | onBeforeMount | 在挂载之前调用 |
mounted | onMounted | 已经挂载到 DOM 上后调用 |
beforeUpdate | onBeforeUpdate | 数据更新之前调用 |
updated | onUpdated | 数据更新后调用 |
beforeDestroy | onBeforeUnmount | 实例销毁之前调用 |
destroyed | onUnmounted | 实例销毁后调用 |
errorCaptured | onErrorCaptured | 组件树中发生错误时被触发 |
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
1 2 3 4 5 6 7 8 9
| import { onMounted} from 'vue' export default{ setup() { onMounted(() => { console.log('component mounted!') }) } }
|
抽离封装
当我们有两个或多个页面采用的是同样的配置或数据,如何抽离出来。
比如同样的数据
1 2 3 4 5
| const res_a = reactive({ name: '马云', age: 50, company: '阿里巴巴' });
|
新建一个通配js配置文件src\stores\public.js
1 2 3 4 5 6 7 8 9 10 11
| import { reactive } from "vue";
export const plbLic = () => { const res_a = reactive({ name: '马云', age: 50, company: '阿里巴巴' }); return res_a; };
|
在App.vue
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <script> import HelloWorld from './components/HelloWorld.vue' import { plbLic } from './stores/public' export default{ name:'App', components:{ HelloWorld }, setup(){ const res_a = plbLic() return{res_a} } } </script>
<template> <div class="back"> 我第一个页面 <h3>{{ res_a.name }}</h3> <h3>{{ res_a.age }}</h3> </div> <HelloWorld/> </template>
<style> .back{ background-color: darkkhaki; padding: 20px 0; } </style>
|
在src\components\HelloWorld.vue
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <script> import { onMounted } from 'vue' import { plbLic } from '../stores/public'
export default { setup() { const res_a = plbLic(); return { res_a }; } } </script>
<template> <div class="back"> 我是第n个页面 <h3>{{ res_a.name }}</h3> <h3>{{ res_a.age }}</h3> </div> </template>
<style scoped> .back{ background-color: brown; color: cyan; padding: 20px 0; } </style>
|
