为什么要使用Composition API?
在 vue3 版本之前,我们复用组件(或者提取和重用多个组件之间的逻辑),通常有以下几种方式:
- Mixin:命名空间冲突 & 渲染上下文中暴露的 property 来源不清晰。例如在阅读一个运用了多个 mixin 的模板时,很难看出某个 property 是从哪一个 mixin 中注入的。
- Renderless Component:无渲染组件需要额外的有状态的组件实例,从而使得性能有所损耗
- Vuex:就会变得更加复杂,需要去定义 Mutations 也需要去定义 Actions
Vue3 中的 Composition API 便是解决这一问题;且完美支持类型推导,不再是依靠一个简单的 this 上下文来暴露 property(比如 methods 选项下的函数的 this 是指向组件实例的,而不是这个 methods 对象)。其是一组低侵入式的、函数式的 API,使得我们能够更灵活地「组合」组件的逻辑。
业务实践
在具体使用上,对vue单文件来说,模板部分和样式部分基本和以前没有区别,组合式api主要影响的是逻辑部分。下面是一个经典的vue2的计数器案例.:
vue2 实现
//Counter.vue
export default {
data: ( => ({
count: 0
},
methods: {
increment( {
this.count++;
}
},
computed: {
double ( {
return this.count * 2;
}
}
}
vue3 composition api
当我们在组件间提取并复用逻辑时,组合式API 是十分灵活的。一个组合函数仅依赖它的参数和 Vue 全局导出的 API,而不是依赖其微妙的 this 上下文。你可以将组件内的任何一段逻辑导出为函数以复用它。
- 基于响应式
- 提供 vue 的生命周期钩子
- 组件销毁时自动销毁依赖监听
- 可复用的逻辑
// Counter.vue
import { ref, computed } from "vue";
export default {
setup( {
const count = ref(0;
const double = computed(( => count * 2
function increment( {
count.value++;
}
return {
count,
double,
increment
}
}
}
代码提取
Composition API的第一个明显优点是提取逻辑很容易。使用Composition提取上面Counter.vue组件代码。
//useCounter.js 组合函数
import { ref, computed } from "vue";
export default function ( {
const count = ref(0;
const double = computed(( => count * 2
function increment( {
count.value++;
}
return {
count,
double,
increment
}
}
代码重用
要在组件中使用该函数,我们只需将模块导入组件文件并调用它(注意导入是一个函数)。这将返回我们定义的变量,随后我们可以从 setup 函数中返回它们。
// MyComponent.js
import useCounter from "./useCounter.js";
export default {
setup( {
const { count, double, increment } = useCounter(;
return {
count,
double,
increment
}
}
}
相比而言,组合式 API:
- 暴露给模板的 property 来源十分清晰,因为它们都是被组合逻辑函数返回的值
- 不存在命名空间冲突,可以通过解构任意命名
- 不再需要仅为逻辑复用而创建新的组件实例
常用api介绍
setup
export default {
setup(props, context {
console.log(context; // { attrs, slots, emit }
//context.emit('emitFun', {emit: true}
return { privateMsg: props.msg };
}
}
setup函数是组件内使用 component API 的入口。是在组件实例被创建时,初始化了 props 之后调用,处于 created 前。还有以下特点:
1.可以返回一个对象或函数,对象的属性会合并到模板渲染的上下文中;
2.第一个参数是响应式的props对象,注意不能解构 props 对象,会使其失去响应性。 **
也不可直接修改 props,会触发警告
3.第二个参数是一个上下文对象,暴露了 attrs,slots,emit 对象
4.this 在 setup 函数中不可用。**因为它不会找到组件实例。setup 的调用发生在 data、computed 和 methods 被解析之前,所以它们无法在 setup 中被获取。
1、props 要先声明才能取值,attrs 不用先声明
2、props 声明过的属性,attrs 里不会再出现
3、props 不包含事件,attrs 包含。vue2中的$listeners 被整合到 $attrs
reactive
<template>
<div>
<p>{{data.msg}}</p>
<button @click="updateData">更新数据</button>
</div>
</template>
<script>
import { reactive } from "vue";
export default {
name: "ReactiveObject",
setup( {
const data = reactive({ msg: "hello world" };
const updateData = ( => {
data.msg= "hello world " + new Date(.getTime(;
};
return { data, updateData };
},
};
</script>
reactive函数接收一个普通对象然后返回对象的响应式代理,同 Vue.observable。
原理:通过proxy对数据进行封装,当数据变化时,触发模板等内容的更新。
ref
<template>
<div>
<p>{{msg}}</p>
<button @click="updateMessage">更新数据</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
name: "ReactiveSingleValue",
setup( {
const msg= ref("hello world";
const updateMessage = ( => {
msg.value = "hello world " + new Date(.getTime(;
};
return { msg, updateMessage };
},
};
</script>
ref和reactive存在一定的相似性,所以需要完全理解它们才能高效的在各种场景下选择不同的方式,它们之间最明显的区别是ref使用的时候需要通过.value来取值,reactive不用。ref是property而reactive是proxy,reactive能够深度监听各种类型对象的变化,ref是处理诸如number,string之类的基本数据类型。
它们的区别也可以这么理解,ref是使某一个数据提供响应能力,而reactive是为包含该数据的一整个对象提供响应能力。
在模板里使用ref和嵌套在响应式对象里时不需要通过.value,会自己解开:
<template>
<div ref="node"></div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup( {
const node = ref(null
onMounted(( => {
console.log(node.value // 此处就是dom元素 <div ref="node"></div>
}
return {
node
}
}
}
</script>
computed
传入一个 getter 函数,返回一个默认不可修改的 ref 对象,同 vue 2.x 中的计算属性 computed
const count = ref(0
const sum = computed(( => count.value + 1
console.log(sum.value // 1
sum.value = 3 // 错误
也可传入一个 get 和 set 函数对象,创建一个可修改的计算状态
const count = ref(0
const sum = computed({
get: ( => count.value + 1,
set: (value => {
count.value = value - 1
}
}
sum.value = 55
console.log(sum, count // 1, 54
watchEffect
import { reactive, watchEffect } from "vue";
export default {
name: "WatchEffect",
setup( {
const data = reactive({ count: 1 };
watchEffect(( => console.log(`侦听器:${data.count}`;
setInterval(( => {
data.count++;
}, 1000;
return { data };
},
};
watchEffect用来监听数据的变化,它会立即执行一次,之后会追踪函数里面用到的所有响应式状态,当变化后会重新执行该回调函数。
watch
- 懒执行副作用;
- 更明确哪些状态的改变会触发侦听器重新运行副作用;
- 访问侦听状态变化前后的值。
// 监听一个 getter
const state = reactive({ count: 0 }
watch(
( => state.count,
(count, prevCount => {
console.log(count, prevCount
}
// 直接监听一个 ref
const count = ref(0
watch(count, (count, prevCount => {
console.log(count, prevCount
}, {
deep: true, // 深度监听
immediate: true // 初始化执行一次
}
// 监听多个数据
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar] => {
console.log([foo, bar], [prevFoo, prevBar]
}
toRefs
把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref,和响应式对象 property 一一对应。可以被解构且保持响应性
<template>
<div>
<h2>解构响应式对象数据</h2>
<p>Username: {{username}}</p>
<p>Age: {{age}}</p>
</div>
</template>
<script>
import { reactive, toRefs } from "vue";
export default {
name: "DestructReactiveObject",
setup( {
const user = reactive({
username: "haihong",
age: 10000,
};
return { ...toRefs(user };
},
};
</script>
toRef
toRef 可以用来为一个 reactive 对象的属性创建一个 ref。这个 ref 可以被传递并且能够保持响应性。
setup( {
const user = reactive({ age: 1 };
const age = toRef(user, "age";
age.value++;
console.log(user.age; // 2
user.age++;
console.log(age.value; // 3
}
Provide/Inject
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive。
当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。
最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup( {
const location = ref('North Pole'
const geolocation = reactive({
longitude: 90,
latitude: 135
}
const updateLocation = ( => {
location.value = 'South Pole'
}
provide('location', readonly(location
provide('geolocation', readonly(geolocation
provide('updateLocation', updateLocation
}
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup( {
const userLocation = inject('location', 'The Universe'
const userGeolocation = inject('geolocation'
const updateUserLocation = inject('updateLocation'
return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}
</script>
生命周期函数
与 2.x 版本生命周期相对应的组合式 API
~~beforeCreate~~ -> 使用 setup(
~~created~~ -> 使用 setup(
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured
只需要将之前的生命周期改成onXXX的形式即可,需要注意的是created、beforeCreate两个钩子被删除了,生命周期函数只能在setup函数里使用。
总结
简单总结一下升级思路,data选项里的数据通过reactive进行声明,通过...toRefs(返回;computed、mounted等选项通过对应的computed、onMounted等函数来进行替换;methods里的函数随便在哪声明,只要在setup函数里返回即可。