為什么要使用Composition API?
根據官方的說法,vue3.0的變化包括性能上的改進、更小的 bundle 體積、對 TypeScript 更好的支持、用于處理大規模用例的全新 API,全新的api指的就是本文主要要說的組合式api。
在 vue3 版本之前,我們復用組件(或者提取和重用多個組件之間的邏輯),通常有以下幾種方式:
- Mixin:命名空間沖突 & 渲染上下文中暴露的 property 來源不清晰。例如在閱讀一個運用了多個 mixin 的模板時,很難看出某個 property 是從哪一個 mixin 中注入的。
- Renderless Component:無渲染組件需要額外的有狀態的組件實例,從而使得性能有所損耗
- Vuex:就會變得更加復雜,需要去定義 Mutations 也需要去定義 Actions
上述提到的幾種方式,也是我們項目中正在使用的方式。對于提取和重用多個組件之間的邏輯似乎并不簡單。我們甚至采用了 extend 來做到最大化利用已有組件邏輯,因此使得代碼邏輯依賴嚴重,難以閱讀和理解。
Vue3 中的 Composition API 便是解決這一問題;且完美支持類型推導,不再是依靠一個簡單的 this 上下文來暴露 property(比如 methods 選項下的函數的 this 是指向組件實例的,而不是這個 methods 對象)。其是一組低侵入式的、函數式的 API,使得我們能夠更靈活地「組合」組件的邏輯。
業務實踐
組合式api的出現就能解決以上兩個問題,此外,它也對TypeScript類型推導更加友好。
在具體使用上,對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 中被獲取。
props與上下文對象attrs的區別:
1、props 要先聲明才能取值,attrs 不用先聲明
2、props 聲明過的屬性,attrs 里不會再出現
3、props 不包含事件,attrs 包含。vue2中的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,會自己解開:
除了響應式ref還有一個引用DOM元素的ref,2.x里面是通過this.$refs.xxx來引用,但是在setup里面沒有this,所以也是通過創建一個ref來使用:
<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
完全等效于 2.x 中 watch 選項,對比 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>
<h1>解構響應式對象數據</h1>
<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函數里使用。
總結
使用組合式api還是需要一點時間來適應的,首先需要能區分ref和reactive,不要在基本類型和引用類型、響應式和非響應式對象之間搞混,其次就是如何拆分好每一個use函數,組合式api帶來了更好的代碼組織方式,但也更容易把代碼寫的更難以維護,比如setup函數巨長。
簡單總結一下升級思路,data選項里的數據通過reactive進行聲明,通過...toRefs()返回;computed、mounted等選項通過對應的computed、onMounted等函數來進行替換;methods里的函數隨便在哪聲明,只要在setup函數里返回即可。